feat(mobile): Add all missing screen registrations and fixes

- Add screen registrations to AppNavigator (P2P, Forum, TaxZekat,
  Launchpad, President, Vote, Validators, Proposals, Identity,
  KurdMedia, Perwerde, B2B)
- Fix supabase.ts with hardcoded fallback credentials for production
- Fix Home tab header (headerShown: false)
- Add new screen components for mini apps
- Update DashboardScreen with proper navigation and alerts
This commit is contained in:
2026-01-16 19:41:23 +03:00
parent 5c74c6306d
commit f89fd43855
26 changed files with 9032 additions and 1605 deletions
+9 -1
View File
@@ -26,7 +26,10 @@
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false,
"permissions": ["android.permission.CAMERA"]
"permissions": [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"
]
},
"plugins": [
[
@@ -38,6 +41,11 @@
],
"web": {
"favicon": "./assets/favicon.png"
},
"extra": {
"eas": {
"projectId": "99a5c55c-03dd-4eec-856d-4586d9ca994b"
}
}
}
}
+6 -3
View File
@@ -1,6 +1,7 @@
{
"cli": {
"version": ">= 13.2.0"
"version": ">= 13.2.0",
"appVersionSource": "local"
},
"build": {
"development": {
@@ -10,12 +11,14 @@
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
"buildType": "apk",
"credentialsSource": "local"
}
},
"production": {
"android": {
"buildType": "apk"
"buildType": "app-bundle",
"credentialsSource": "local"
}
}
},
+8
View File
@@ -46,4 +46,12 @@ if (!config.resolver.sourceExts.includes('svg')) {
// Polyfills will be resolved from project's own node_modules
// ============================================
// PACKAGE EXPORTS RESOLUTION
// ============================================
// Disable strict package exports checking for packages like @noble/hashes
// that don't properly export all their submodules
config.resolver.unstable_enablePackageExports = false;
module.exports = config;
+1 -1
View File
@@ -90,7 +90,7 @@
},
"devDependencies": {
"@expo/ngrok": "^4.1.0",
"@pezkuwi/extension-dapp": "0.62.14",
"@pezkuwi/extension-dapp": "0.62.17",
"@pezkuwi/extension-inject": "0.62.14",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.3.3",
+42 -4
View File
@@ -10,10 +10,16 @@ import {
Alert,
} from 'react-native';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import { useFocusEffect } from '@react-navigation/native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import type { NavigationProp } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi } from '../contexts/PezkuwiContext';
type RootStackParamList = {
Wallet: undefined;
WalletSetup: undefined;
};
// Base URL for the web app
const WEB_BASE_URL = 'https://pezkuwichain.io';
@@ -41,6 +47,7 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
const [error, setError] = useState<string | null>(null);
const [canGoBack, setCanGoBack] = useState(false);
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { selectedAccount, getKeyPair, api, isApiReady } = usePezkuwi();
// JavaScript to inject into the WebView
@@ -214,9 +221,40 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
break;
case 'CONNECT_WALLET':
// Trigger native wallet connection
// This would open a modal or navigate to wallet screen
// Handle wallet connection request from WebView
if (__DEV__) console.log('WebView requested wallet connection');
if (selectedAccount) {
// Already connected, notify WebView
webViewRef.current?.injectJavaScript(`
window.PEZKUWI_ADDRESS = '${selectedAccount.address}';
window.PEZKUWI_ACCOUNT_NAME = '${selectedAccount.meta?.name || 'Mobile Wallet'}';
window.dispatchEvent(new CustomEvent('pezkuwi-wallet-connected', {
detail: {
address: '${selectedAccount.address}',
name: '${selectedAccount.meta?.name || 'Mobile Wallet'}'
}
}));
`);
} else {
// No wallet connected, show alert and navigate to wallet setup
Alert.alert(
'Wallet Required',
'Please connect or create a wallet to continue.',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Setup Wallet',
onPress: () => {
navigation.navigate('WalletSetup');
},
},
]
);
}
break;
case 'GO_BACK':
@@ -243,7 +281,7 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
console.error('Failed to parse WebView message:', parseError);
}
}
}, [selectedAccount, getKeyPair, canGoBack]);
}, [selectedAccount, getKeyPair, canGoBack, navigation, api, isApiReady]);
// Handle Android back button
useFocusEffect(
+7 -10
View File
@@ -5,21 +5,18 @@
* Used for: Forum, P2P Platform, Notifications, Referrals
*/
import 'react-native-url-polyfill/auto';
// Note: react-native-url-polyfill removed - React Native 0.81+ has native URL support
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { ENV } from '../config/environment';
// Initialize Supabase client from environment variables
const supabaseUrl = ENV.supabaseUrl || '';
const supabaseKey = ENV.supabaseAnonKey || '';
// Hardcoded fallbacks for production builds where ENV may not be available
const FALLBACK_SUPABASE_URL = 'https://vsyrpfiwhjvahofxwytr.supabase.co';
const FALLBACK_SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZzeXJwZml3aGp2YWhvZnh3eXRyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjAwMjYxNTgsImV4cCI6MjA3NTYwMjE1OH0.dO2c8YWIph2D95X7jFdlGYJ8MXyuyorkLcjQ6onH-HE';
if (!supabaseUrl || !supabaseKey) {
if (__DEV__) {
console.warn('⚠️ [Supabase] Credentials not found in environment variables');
console.warn('Add EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to .env');
}
}
// Initialize Supabase client from environment variables with fallbacks
const supabaseUrl = ENV.supabaseUrl || FALLBACK_SUPABASE_URL;
const supabaseKey = ENV.supabaseAnonKey || FALLBACK_SUPABASE_KEY;
// Create Supabase client
export const supabase = createClient(supabaseUrl, supabaseKey, {
+84
View File
@@ -19,6 +19,18 @@ import EditProfileScreen from '../screens/EditProfileScreen';
import WalletScreen from '../screens/WalletScreen';
import WalletSetupScreen from '../screens/WalletSetupScreen';
import SwapScreen from '../screens/SwapScreen';
import P2PScreen from '../screens/P2PScreen';
import ForumScreen from '../screens/ForumScreen';
import TaxZekatScreen from '../screens/TaxZekatScreen';
import LaunchpadScreen from '../screens/LaunchpadScreen';
import PresidentScreen from '../screens/PresidentScreen';
import VoteScreen from '../screens/VoteScreen';
import ValidatorsScreen from '../screens/ValidatorsScreen';
import ProposalsScreen from '../screens/ProposalsScreen';
import IdentityScreen from '../screens/IdentityScreen';
import KurdMediaScreen from '../screens/KurdMediaScreen';
import PerwerdeScreen from '../screens/PerwerdeScreen';
import B2BScreen from '../screens/B2BScreen';
export type RootStackParamList = {
Welcome: undefined;
@@ -33,6 +45,18 @@ export type RootStackParamList = {
BeCitizenChoice: undefined;
BeCitizenApply: undefined;
BeCitizenClaim: undefined;
P2P: undefined;
Forum: undefined;
TaxZekat: undefined;
Launchpad: undefined;
President: undefined;
Vote: undefined;
Validators: undefined;
Proposals: undefined;
Identity: undefined;
KurdMedia: undefined;
Perwerde: undefined;
B2B: undefined;
};
const Stack = createStackNavigator<RootStackParamList>();
@@ -155,6 +179,66 @@ const AppNavigator: React.FC = () => {
headerShown: false,
}}
/>
<Stack.Screen
name="P2P"
component={P2PScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Forum"
component={ForumScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="TaxZekat"
component={TaxZekatScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Launchpad"
component={LaunchpadScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="President"
component={PresidentScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Vote"
component={VoteScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Validators"
component={ValidatorsScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Proposals"
component={ProposalsScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Identity"
component={IdentityScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="KurdMedia"
component={KurdMediaScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Perwerde"
component={PerwerdeScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="B2B"
component={B2BScreen}
options={{ headerShown: false }}
/>
</>
)}
</Stack.Navigator>
+1 -1
View File
@@ -65,7 +65,7 @@ const BottomTabNavigator: React.FC = () => {
name="Home"
component={DashboardScreen}
options={{
header: (props) => <GradientHeader {...props} />,
headerShown: false,
tabBarLabel: 'Home',
tabBarIcon: ({ color, focused }) => (
<Text style={[styles.icon, { color }]}>
File diff suppressed because it is too large Load Diff
+12 -9
View File
@@ -14,12 +14,9 @@ import {
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import {
submitKycApplication,
uploadToIPFS,
FOUNDER_ADDRESS,
} from '../../shared/lib/citizenship-workflow';
import { uploadToIPFS, FOUNDER_ADDRESS } from '../../shared/lib/citizenship-workflow';
import type { Region, MaritalStatus } from '../../shared/lib/citizenship-workflow';
import { submitKycApplicationMobile } from '../utils/citizenship';
import { KurdistanColors } from '../theme/colors';
// Temporary custom picker component (until we fix @react-native-picker/picker installation)
@@ -97,7 +94,7 @@ const CustomPicker: React.FC<{
const BeCitizenApplyScreen: React.FC = () => {
const navigation = useNavigation();
const { api, selectedAccount } = usePezkuwi();
const { api, selectedAccount, getKeyPair } = usePezkuwi();
const [isSubmitting, setIsSubmitting] = useState(false);
// Form State
@@ -181,10 +178,16 @@ const BeCitizenApplyScreen: React.FC = () => {
throw new Error('Failed to upload data to IPFS');
}
// Step 2: Submit KYC application to blockchain
const result = await submitKycApplication(
// Step 2: Get keyPair for signing
const keyPair = await getKeyPair(selectedAccount.address);
if (!keyPair) {
throw new Error('Could not retrieve key pair for signing');
}
// Step 3: Submit KYC application to blockchain
const result = await submitKycApplicationMobile(
api,
selectedAccount,
keyPair,
citizenshipData.fullName,
citizenshipData.email,
String(ipfsCid),
+17 -573
View File
@@ -1,413 +1,23 @@
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
ScrollView,
StatusBar,
TextInput,
Alert,
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import {
submitKycApplication,
uploadToIPFS,
getCitizenshipStatus,
} from '../../shared/lib/citizenship-workflow';
import { KurdistanColors } from '../theme/colors';
import React from 'react';
import { SafeAreaView, StyleSheet } from 'react-native';
import { PezkuwiWebView } from '../components';
/**
* Be Citizen Screen
*
* Uses WebView to load the citizenship application interface from the web app.
* The web app handles all citizenship logic (new application, existing citizen verification).
* Native wallet bridge allows transaction signing from the mobile app.
*
* Citizenship status is checked at governance action entry points.
*/
const BeCitizenScreen: React.FC = () => {
const { api, selectedAccount } = usePezkuwi();
const [_isExistingCitizen, _setIsExistingCitizen] = useState(false);
const [currentStep, setCurrentStep] = useState<'choice' | 'new' | 'existing'>('choice');
const [isSubmitting, setIsSubmitting] = useState(false);
// New Citizen Form State
const [fullName, setFullName] = useState('');
const [fatherName, setFatherName] = useState('');
const [motherName, setMotherName] = useState('');
const [tribe, setTribe] = useState('');
const [region, setRegion] = useState('');
const [email, setEmail] = useState('');
const [profession, setProfession] = useState('');
const [referralCode, setReferralCode] = useState('');
// Existing Citizen Login State
const [citizenId, setCitizenId] = useState('');
const [password, setPassword] = useState('');
const handleNewCitizenApplication = async () => {
if (!fullName || !fatherName || !motherName || !email) {
Alert.alert('Error', 'Please fill in all required fields');
return;
}
if (!api || !selectedAccount) {
Alert.alert('Error', 'Please connect your wallet first');
return;
}
setIsSubmitting(true);
try {
// Prepare citizenship data
const citizenshipData = {
fullName,
fatherName,
motherName,
tribe,
region,
email,
profession,
referralCode,
walletAddress: selectedAccount.address,
timestamp: Date.now(),
};
// Step 1: Upload encrypted data to IPFS
const ipfsCid = await uploadToIPFS(citizenshipData);
if (!ipfsCid) {
throw new Error('Failed to upload data to IPFS');
}
// Step 2: Submit KYC application to blockchain
const result = await submitKycApplication(
api,
selectedAccount,
fullName,
email,
ipfsCid,
'Citizenship application via mobile app'
);
if (result.success) {
Alert.alert(
'Application Submitted!',
'Your citizenship application has been submitted for review. You will receive a confirmation once approved.',
[
{
text: 'OK',
onPress: () => {
// Reset form
setFullName('');
setFatherName('');
setMotherName('');
setTribe('');
setRegion('');
setEmail('');
setProfession('');
setReferralCode('');
setCurrentStep('choice');
},
},
]
);
} else {
Alert.alert('Application Failed', result.error || 'Failed to submit application');
}
} catch (error: unknown) {
if (__DEV__) console.error('Citizenship application error:', error);
Alert.alert('Error', error instanceof Error ? error.message : 'An unexpected error occurred');
} finally {
setIsSubmitting(false);
}
};
const handleExistingCitizenLogin = async () => {
if (!api || !selectedAccount) {
Alert.alert('Error', 'Please connect your wallet first');
return;
}
setIsSubmitting(true);
try {
const status = await getCitizenshipStatus(api, selectedAccount.address);
if (status.kycStatus === 'Approved' && status.hasCitizenTiki) {
Alert.alert(
'Success',
`Welcome back, Citizen!\n\nYour Tiki Number: ${status.tikiNumber || 'N/A'}`,
[
{
text: 'OK',
onPress: () => {
setCitizenId('');
setPassword('');
setCurrentStep('choice');
},
},
]
);
} else if (status.kycStatus === 'Approved' && !status.hasCitizenTiki) {
Alert.alert(
'Almost there!',
'Your KYC is approved, but you haven\'t claimed your Citizen Tiki yet. Please claim it on the web portal.',
[{ text: 'OK' }]
);
} else if (status.kycStatus === 'Pending') {
Alert.alert(
'Application Pending',
'Your citizenship application is still under review.',
[{ text: 'OK' }]
);
} else {
Alert.alert(
'Not a Citizen',
'We couldn\'t find a citizenship record for this wallet. If you have a Citizen ID and Password, please note that wallet-based verification is now preferred.',
[{ text: 'OK' }]
);
}
} catch (error: unknown) {
if (__DEV__) console.error('Citizenship verification error:', error);
Alert.alert('Error', 'Failed to verify citizenship status');
} finally {
setIsSubmitting(false);
}
};
if (currentStep === 'choice') {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<LinearGradient
colors={[KurdistanColors.kesk, KurdistanColors.zer, KurdistanColors.sor]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.gradient}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.header}>
<View style={styles.logoContainer}>
<Text style={styles.logoText}>🏛</Text>
</View>
<Text style={styles.title}>Be a Citizen</Text>
<Text style={styles.subtitle}>
Join the Pezkuwi decentralized nation
</Text>
</View>
<View style={styles.choiceContainer}>
<TouchableOpacity
style={styles.choiceCard}
onPress={() => setCurrentStep('new')}
activeOpacity={0.8}
>
<Text style={styles.choiceIcon}>📝</Text>
<Text style={styles.choiceTitle}>New Citizen</Text>
<Text style={styles.choiceDescription}>
Apply for citizenship and join our community
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.choiceCard}
onPress={() => setCurrentStep('existing')}
activeOpacity={0.8}
>
<Text style={styles.choiceIcon}>🔐</Text>
<Text style={styles.choiceTitle}>Existing Citizen</Text>
<Text style={styles.choiceDescription}>
Access your citizenship account
</Text>
</TouchableOpacity>
</View>
<View style={styles.infoSection}>
<Text style={styles.infoTitle}>Citizenship Benefits</Text>
<View style={styles.benefitItem}>
<Text style={styles.benefitIcon}></Text>
<Text style={styles.benefitText}>Voting rights in governance</Text>
</View>
<View style={styles.benefitItem}>
<Text style={styles.benefitIcon}></Text>
<Text style={styles.benefitText}>Access to exclusive services</Text>
</View>
<View style={styles.benefitItem}>
<Text style={styles.benefitIcon}></Text>
<Text style={styles.benefitText}>Referral rewards program</Text>
</View>
<View style={styles.benefitItem}>
<Text style={styles.benefitIcon}></Text>
<Text style={styles.benefitText}>Community recognition</Text>
</View>
</View>
</ScrollView>
</LinearGradient>
</SafeAreaView>
);
}
if (currentStep === 'new') {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
<ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}>
<TouchableOpacity
style={styles.backButton}
onPress={() => setCurrentStep('choice')}
>
<Text style={styles.backButtonText}> Back</Text>
</TouchableOpacity>
<Text style={styles.formTitle}>New Citizen Application</Text>
<Text style={styles.formSubtitle}>
Please provide your information to apply for citizenship
</Text>
<View style={styles.inputGroup}>
<Text style={styles.label}>Full Name *</Text>
<TextInput
style={styles.input}
placeholder="Enter your full name"
value={fullName}
onChangeText={setFullName}
placeholderTextColor="#999"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Father&apos;s Name *</Text>
<TextInput
style={styles.input}
placeholder="Enter father's name"
value={fatherName}
onChangeText={setFatherName}
placeholderTextColor="#999"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Mother&apos;s Name *</Text>
<TextInput
style={styles.input}
placeholder="Enter mother's name"
value={motherName}
onChangeText={setMotherName}
placeholderTextColor="#999"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Tribe</Text>
<TextInput
style={styles.input}
placeholder="Enter tribe (optional)"
value={tribe}
onChangeText={setTribe}
placeholderTextColor="#999"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Region</Text>
<TextInput
style={styles.input}
placeholder="Enter region (optional)"
value={region}
onChangeText={setRegion}
placeholderTextColor="#999"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email *</Text>
<TextInput
style={styles.input}
placeholder="Enter email address"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
placeholderTextColor="#999"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Profession</Text>
<TextInput
style={styles.input}
placeholder="Enter profession (optional)"
value={profession}
onChangeText={setProfession}
placeholderTextColor="#999"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Referral Code</Text>
<TextInput
style={styles.input}
placeholder="Enter referral code (optional)"
value={referralCode}
onChangeText={setReferralCode}
placeholderTextColor="#999"
/>
</View>
<TouchableOpacity
style={[styles.submitButton, isSubmitting && styles.submitButtonDisabled]}
onPress={handleNewCitizenApplication}
activeOpacity={0.8}
disabled={isSubmitting}
>
{isSubmitting ? (
<ActivityIndicator color={KurdistanColors.spi} />
) : (
<Text style={styles.submitButtonText}>Submit Application</Text>
)}
</TouchableOpacity>
<View style={styles.spacer} />
</ScrollView>
</SafeAreaView>
);
}
// Existing Citizen Login
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
<ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}>
<TouchableOpacity
style={styles.backButton}
onPress={() => setCurrentStep('choice')}
>
<Text style={styles.backButtonText}> Back</Text>
</TouchableOpacity>
<Text style={styles.formTitle}>Citizen Verification</Text>
<Text style={styles.formSubtitle}>
Verify your status using your connected wallet
</Text>
<View style={styles.infoCard}>
<Text style={styles.infoText}>
Existing citizens are verified through their blockchain identity. Ensure your citizenship wallet is selected in the wallet tab.
</Text>
</View>
<TouchableOpacity
style={[styles.submitButton, isSubmitting && styles.submitButtonDisabled]}
onPress={handleExistingCitizenLogin}
activeOpacity={0.8}
disabled={isSubmitting}
>
{isSubmitting ? (
<ActivityIndicator color={KurdistanColors.spi} />
) : (
<Text style={styles.submitButtonText}>Verify Citizenship</Text>
)}
</TouchableOpacity>
</ScrollView>
<PezkuwiWebView
path="/be-citizen"
title="Be Citizen"
/>
</SafeAreaView>
);
};
@@ -415,173 +25,7 @@ const BeCitizenScreen: React.FC = () => {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
gradient: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
padding: 20,
paddingTop: 60,
},
header: {
alignItems: 'center',
marginBottom: 40,
},
logoContainer: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: KurdistanColors.spi,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.3)',
elevation: 8,
},
logoText: {
fontSize: 48,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: KurdistanColors.spi,
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: KurdistanColors.spi,
textAlign: 'center',
opacity: 0.9,
},
choiceContainer: {
gap: 16,
marginBottom: 40,
},
choiceCard: {
backgroundColor: KurdistanColors.spi,
borderRadius: 20,
padding: 24,
alignItems: 'center',
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.2)',
elevation: 6,
},
choiceIcon: {
fontSize: 48,
marginBottom: 16,
},
choiceTitle: {
fontSize: 20,
fontWeight: 'bold',
color: KurdistanColors.kesk,
marginBottom: 8,
},
choiceDescription: {
fontSize: 14,
color: '#666',
textAlign: 'center',
},
infoSection: {
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 16,
padding: 20,
},
infoTitle: {
fontSize: 18,
fontWeight: '600',
color: KurdistanColors.spi,
marginBottom: 16,
},
benefitItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
benefitIcon: {
fontSize: 16,
color: KurdistanColors.spi,
marginRight: 12,
fontWeight: 'bold',
},
benefitText: {
fontSize: 14,
color: KurdistanColors.spi,
flex: 1,
},
infoCard: {
backgroundColor: `${KurdistanColors.kesk}15`,
padding: 16,
borderRadius: 12,
marginBottom: 24,
borderLeftWidth: 4,
borderLeftColor: KurdistanColors.kesk,
},
infoText: {
fontSize: 14,
color: KurdistanColors.reş,
lineHeight: 20,
opacity: 0.8,
},
formContainer: {
flex: 1,
padding: 20,
},
backButton: {
marginBottom: 20,
},
backButtonText: {
fontSize: 16,
color: KurdistanColors.kesk,
fontWeight: '600',
},
formTitle: {
fontSize: 24,
fontWeight: 'bold',
color: KurdistanColors.reş,
marginBottom: 8,
},
formSubtitle: {
fontSize: 14,
color: '#666',
marginBottom: 24,
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: KurdistanColors.reş,
marginBottom: 8,
},
input: {
backgroundColor: KurdistanColors.spi,
borderRadius: 12,
padding: 16,
fontSize: 16,
borderWidth: 1,
borderColor: '#E0E0E0',
},
submitButton: {
backgroundColor: KurdistanColors.kesk,
borderRadius: 12,
padding: 16,
alignItems: 'center',
marginTop: 20,
boxShadow: '0px 4px 6px rgba(0, 128, 0, 0.3)',
elevation: 6,
},
submitButtonDisabled: {
opacity: 0.6,
},
submitButtonText: {
fontSize: 18,
fontWeight: 'bold',
color: KurdistanColors.spi,
},
spacer: {
height: 40,
backgroundColor: '#FFFFFF',
},
});
+61 -25
View File
@@ -23,6 +23,7 @@ import { useAuth } from '../contexts/AuthContext';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { supabase } from '../lib/supabase';
import AvatarPickerModal from '../components/AvatarPickerModal';
import { NotificationCenterModal } from '../components/NotificationCenterModal';
import { fetchUserTikis, getPrimaryRole, getTikiDisplayName, getTikiEmoji, getTikiColor } from '../../shared/lib/tiki';
import { getAllScores, type UserScores } from '../../shared/lib/scores';
import { getKycStatus } from '../../shared/lib/kyc';
@@ -87,6 +88,7 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
const [profileData, setProfileData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [avatarModalVisible, setAvatarModalVisible] = useState(false);
const [notificationModalVisible, setNotificationModalVisible] = useState(false);
// Blockchain state
const [tikis, setTikis] = useState<string[]>([]);
@@ -175,6 +177,38 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
);
};
const showAwaitingGovernment = () => {
Alert.alert(
'Li benda damezrandinê / Awaiting Establishment',
'Duaye helbejartina hukumeta Komara Dijitaliya Kurdistanê yên beta damezrandin.\n\nAwaiting the beta elections and establishment of the Digital Kurdistan Republic government.',
[{ text: 'Temam / OK' }]
);
};
const showUnderMaintenance = () => {
Alert.alert(
'Di bin çêkirinê de ye / Under Maintenance',
'Ev taybetmendî niha di bin çêkirinê de ye. Ji kerema xwe paşê vegerin.\n\nThis feature is currently under maintenance. Please check back later.',
[{ text: 'Temam / OK' }]
);
};
const showAwaitingSerokElection = () => {
Alert.alert(
'Li benda hilbijartinên çalak / Awaiting Active Elections',
'Duaye hilbijartinên Serokî yên çalak bibin.\n\nAwaiting active Presidential elections to be initiated.',
[{ text: 'Temam / OK' }]
);
};
const showAwaitingMinistryOfEducation = () => {
Alert.alert(
'Li benda Wezareta Perwerdê / Awaiting Ministry of Education',
'Duaye damezrandina Wezareta Perwerdê yên aktîv bibin.\n\nAwaiting the establishment of an active Ministry of Education.',
[{ text: 'Temam / OK' }]
);
};
const handleAvatarClick = () => {
setAvatarModalVisible(true);
};
@@ -268,7 +302,7 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
</View>
<View style={styles.headerActions}>
<TouchableOpacity style={styles.iconButton} onPress={() => showComingSoon('Notifications')}>
<TouchableOpacity style={styles.iconButton} onPress={() => setNotificationModalVisible(true)}>
<Text style={styles.headerIcon}>🔔</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.iconButton} onPress={() => navigation.navigate('Settings')}>
@@ -411,7 +445,7 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
{kycStatus === 'NotStarted' && (
<TouchableOpacity
style={styles.kycButton}
onPress={() => navigation.navigate('BeCitizenChoice')}
onPress={() => navigation.navigate('BeCitizen')}
>
<Text style={styles.kycButtonText}>Apply</Text>
</TouchableOpacity>
@@ -432,12 +466,12 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
{/* Wallet - Navigate to WalletScreen */}
{renderAppIcon('Wallet', '👛', () => navigation.navigate('Wallet'), true)}
{renderAppIcon('Bank', qaBank, () => showComingSoon('Bank'), false, true)}
{renderAppIcon('Exchange', qaExchange, () => showComingSoon('Swap'), false)}
{renderAppIcon('P2P', qaTrading, () => showComingSoon('P2P'), false)}
{renderAppIcon('B2B', qaB2B, () => showComingSoon('B2B Trading'), false, true)}
{renderAppIcon('Tax', '📊', () => showComingSoon('Tax/Zekat'), true, true)}
{renderAppIcon('Launchpad', '🚀', () => showComingSoon('Launchpad'), true, true)}
{renderAppIcon('Bank', qaBank, () => showAwaitingGovernment(), false, true)}
{renderAppIcon('Exchange', qaExchange, () => navigation.navigate('Swap'), false)}
{renderAppIcon('P2P', qaTrading, () => navigation.navigate('P2P'), false)}
{renderAppIcon('B2B', qaB2B, () => navigation.navigate('B2B'), false, true)}
{renderAppIcon('Bac/Zekat', '📊', () => navigation.navigate('TaxZekat'), true)}
{renderAppIcon('Launchpad', '🚀', () => navigation.navigate('Launchpad'), true, true)}
</View>
</View>
@@ -447,14 +481,14 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
<Text style={styles.sectionTitle}>GOVERNANCE 🏛</Text>
</View>
<View style={styles.appsGrid}>
{renderAppIcon('President', '👑', () => showComingSoon('Presidency'), true, true)}
{renderAppIcon('Assembly', qaGovernance, () => showComingSoon('Assembly'), false, true)}
{renderAppIcon('Vote', '🗳️', () => showComingSoon('Voting'), true, true)}
{renderAppIcon('Validators', '🛡️', () => showComingSoon('Validators'), true, true)}
{renderAppIcon('Justice', '⚖️', () => showComingSoon('Dad / Justice'), true, true)}
{renderAppIcon('Proposals', '📜', () => showComingSoon('Proposals'), true, true)}
{renderAppIcon('President', '👑', () => navigation.navigate('President'), true)}
{renderAppIcon('Assembly', qaGovernance, () => showUnderMaintenance(), false, true)}
{renderAppIcon('Vote', '🗳️', () => navigation.navigate('Vote'), true)}
{renderAppIcon('Validators', '🛡️', () => navigation.navigate('Validators'), true)}
{renderAppIcon('Justice', '⚖️', () => showAwaitingSerokElection(), true, true)}
{renderAppIcon('Proposals', '📜', () => navigation.navigate('Proposals'), true)}
{renderAppIcon('Polls', '📊', () => showComingSoon('Public Polls'), true, true)}
{renderAppIcon('Identity', '🆔', () => navigation.navigate('BeCitizenChoice'), true)}
{renderAppIcon('Identity', '🆔', () => navigation.navigate('Identity'), true)}
</View>
</View>
@@ -465,8 +499,8 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
</View>
<View style={styles.appsGrid}>
{renderAppIcon('whatsKURD', '💬', () => showComingSoon('whatsKURD'), true, true)}
{renderAppIcon('Forum', qaForum, () => showComingSoon('Forum'), false)}
{renderAppIcon('KurdMedia', qaKurdMedia, () => showComingSoon('KurdMedia'), false, true)}
{renderAppIcon('Forum', qaForum, () => navigation.navigate('Forum'), false)}
{renderAppIcon('KurdMedia', qaKurdMedia, () => navigation.navigate('KurdMedia'), false)}
{renderAppIcon('Events', '🎭', () => showComingSoon('Çalakî / Events'), true, true)}
{renderAppIcon('Help', '🤝', () => showComingSoon('Harîkarî / Help'), true, true)}
{renderAppIcon('Music', '🎵', () => showComingSoon('Music Stream'), true, true)}
@@ -481,14 +515,10 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
<Text style={styles.sectionTitle}>EDUCATION 📚</Text>
</View>
<View style={styles.appsGrid}>
{renderAppIcon('University', qaUniversity, () => showComingSoon('University'), false, true)}
{renderAppIcon('Perwerde', qaEducation, () => showComingSoon('Education'), false)}
{renderAppIcon('Library', '📜', () => showComingSoon('Pirtûkxane'), true, true)}
{renderAppIcon('Language', '🗣️', () => showComingSoon('Ziman / Language'), true, true)}
{renderAppIcon('Kids', '🧸', () => showComingSoon('Zarok / Kids'), true, true)}
{renderAppIcon('Certificates', '🏆', () => showComingSoon('Certificates'), true, true)}
{renderAppIcon('Research', '🔬', () => showComingSoon('Research'), true, true)}
{renderAppIcon('History', '🏺', () => showComingSoon('History'), true, true)}
{renderAppIcon('University', qaUniversity, () => showAwaitingMinistryOfEducation(), false, true)}
{renderAppIcon('Perwerde', qaEducation, () => navigation.navigate('Perwerde'), false)}
{renderAppIcon('Certificates', '🏆', () => showAwaitingMinistryOfEducation(), true, true)}
{renderAppIcon('Research', '🔬', () => showAwaitingMinistryOfEducation(), true, true)}
</View>
</View>
@@ -502,6 +532,12 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
currentAvatar={profileData?.avatar_url}
onAvatarSelected={handleAvatarSelected}
/>
{/* Notification Center Modal */}
<NotificationCenterModal
visible={notificationModalVisible}
onClose={() => setNotificationModalVisible(false)}
/>
</SafeAreaView>
);
};
File diff suppressed because it is too large Load Diff
+543
View File
@@ -0,0 +1,543 @@
import React from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
ScrollView,
StatusBar,
Alert,
Linking,
Image,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useNavigation } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
// Media channel types
interface MediaChannel {
id: string;
name: string;
nameKu: string;
icon: string;
description: string;
descriptionKu: string;
color: string;
}
// Social platform types
interface SocialPlatform {
id: string;
name: string;
icon: string;
url: string;
color: string;
}
// Kurdish Media Channels (DKS - Digital Kurdistan State)
const MEDIA_CHANNELS: MediaChannel[] = [
{
id: 'dkstv',
name: 'DKS TV',
nameKu: 'DKS TV',
icon: '📺',
description: 'Digital Kurdistan State Television',
descriptionKu: 'Televizyona Dewleta Dijîtal a Kurdistanê',
color: '#E53935',
},
{
id: 'dksgzt',
name: 'DKS Gazette',
nameKu: 'DKS Rojname',
icon: '📰',
description: 'Official News & Announcements',
descriptionKu: 'Nûçe û Daxuyaniyên Fermî',
color: '#1E88E5',
},
{
id: 'dksradio',
name: 'DKS Radio',
nameKu: 'DKS Radyo',
icon: '📻',
description: 'Digital Kurdistan State Radio',
descriptionKu: 'Radyoya Dewleta Dijîtal a Kurdistanê',
color: '#7B1FA2',
},
{
id: 'dksmusic',
name: 'DKS Music',
nameKu: 'DKS Muzîk',
icon: '🎵',
description: 'Kurdish Music Streaming',
descriptionKu: 'Weşana Muzîka Kurdî',
color: '#00897B',
},
{
id: 'dkspodcast',
name: 'DKS Podcast',
nameKu: 'DKS Podcast',
icon: '🎙️',
description: 'Kurdish Podcasts & Talks',
descriptionKu: 'Podcast û Gotûbêjên Kurdî',
color: '#F4511E',
},
{
id: 'dksdocs',
name: 'DKS Docs',
nameKu: 'DKS Belgefîlm',
icon: '🎬',
description: 'Documentaries & Films',
descriptionKu: 'Belgefîlm û Fîlim',
color: '#6D4C41',
},
];
// PezkuwiChain Social Platforms
const SOCIAL_PLATFORMS: SocialPlatform[] = [
{
id: 'telegram',
name: 'Telegram',
icon: '✈️',
url: 'https://t.me/pezkuwichain',
color: '#0088CC',
},
{
id: 'discord',
name: 'Discord',
icon: '💬',
url: 'https://discord.gg/Y3VyEC6h8W',
color: '#5865F2',
},
{
id: 'twitter',
name: 'X (Twitter)',
icon: '🐦',
url: 'https://twitter.com/pezkuwichain',
color: '#1DA1F2',
},
{
id: 'facebook',
name: 'Facebook',
icon: '📘',
url: 'https://www.facebook.com/profile.php?id=61582484611719',
color: '#1877F2',
},
{
id: 'medium',
name: 'Medium',
icon: '📝',
url: 'https://medium.com/@pezkuwichain',
color: '#000000',
},
{
id: 'github',
name: 'GitHub',
icon: '💻',
url: 'https://github.com/pezkuwichain',
color: '#333333',
},
];
const KurdMediaScreen: React.FC = () => {
const navigation = useNavigation();
const handleMediaPress = (channel: MediaChannel) => {
Alert.alert(
`${channel.nameKu} - Tê de ye / Coming Soon`,
`${channel.descriptionKu}\n\n${channel.description}\n\nEv taybetmendî di pêşveçûnê de ye.\nThis feature is under development.`,
[{ text: 'Temam / OK' }]
);
};
const handleSocialPress = async (platform: SocialPlatform) => {
try {
const canOpen = await Linking.canOpenURL(platform.url);
if (canOpen) {
await Linking.openURL(platform.url);
} else {
Alert.alert(
'Xeletî / Error',
`Nikarim ${platform.name} vebikum.\nCannot open ${platform.name}.`,
[{ text: 'Temam / OK' }]
);
}
} catch (error) {
Alert.alert('Xeletî / Error', 'Tiştek xelet çû.\nSomething went wrong.');
}
};
const renderMediaChannel = (channel: MediaChannel) => (
<TouchableOpacity
key={channel.id}
style={styles.mediaCard}
onPress={() => handleMediaPress(channel)}
activeOpacity={0.7}
>
<View style={[styles.mediaIconContainer, { backgroundColor: channel.color }]}>
<Text style={styles.mediaIcon}>{channel.icon}</Text>
</View>
<View style={styles.mediaInfo}>
<Text style={styles.mediaName}>{channel.nameKu}</Text>
<Text style={styles.mediaDescription} numberOfLines={1}>
{channel.descriptionKu}
</Text>
</View>
<View style={styles.comingSoonBadge}>
<Text style={styles.comingSoonText}>Soon</Text>
</View>
</TouchableOpacity>
);
const renderSocialPlatform = (platform: SocialPlatform) => (
<TouchableOpacity
key={platform.id}
style={styles.socialButton}
onPress={() => handleSocialPress(platform)}
activeOpacity={0.7}
>
<View style={[styles.socialIconContainer, { backgroundColor: platform.color }]}>
<Text style={styles.socialIcon}>{platform.icon}</Text>
</View>
<Text style={styles.socialName}>{platform.name}</Text>
</TouchableOpacity>
);
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
{/* Header */}
<LinearGradient
colors={[KurdistanColors.sor, '#C62828']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.header}
>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Text style={styles.backButtonText}></Text>
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.headerTitle}>KurdMedia</Text>
<Text style={styles.headerSubtitle}>Medyaya Kurdî & Piştgirî</Text>
</View>
</LinearGradient>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
{/* Kurdish Media Section */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<View style={styles.sectionIconContainer}>
<Text style={styles.sectionIcon}>📡</Text>
</View>
<View>
<Text style={styles.sectionTitle}>Medyaya Kurdî</Text>
<Text style={styles.sectionSubtitle}>Kurdish Media</Text>
</View>
</View>
<View style={styles.sectionCard}>
<Text style={styles.sectionDescription}>
Weşanên fermî yên Dewleta Dijîtal a Kurdistanê. TV, radyo, nûçe û bêtir.
</Text>
<Text style={styles.sectionDescriptionEn}>
Official broadcasts of Digital Kurdistan State. TV, radio, news and more.
</Text>
<View style={styles.mediaList}>
{MEDIA_CHANNELS.map(renderMediaChannel)}
</View>
</View>
</View>
{/* Support PezkuwiChain Section */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<View style={[styles.sectionIconContainer, { backgroundColor: KurdistanColors.kesk }]}>
<Text style={styles.sectionIcon}>🤝</Text>
</View>
<View>
<Text style={styles.sectionTitle}>Piştgirî PezkuwiChain</Text>
<Text style={styles.sectionSubtitle}>Support PezkuwiChain</Text>
</View>
</View>
<View style={styles.sectionCard}>
<Text style={styles.sectionDescription}>
Bi me re têkildar bin li ser platformên civakî. Pirsan bipirsin, nûçeyan bişopînin û bên beşdarî civata me.
</Text>
<Text style={styles.sectionDescriptionEn}>
Connect with us on social platforms. Ask questions, follow news and join our community.
</Text>
<View style={styles.socialGrid}>
{SOCIAL_PLATFORMS.map(renderSocialPlatform)}
</View>
{/* Community Stats */}
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={styles.statValue}>40M+</Text>
<Text style={styles.statLabel}>Kurd li cîhanê</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statItem}>
<Text style={styles.statValue}>5B</Text>
<Text style={styles.statLabel}>PEZ Total</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statItem}>
<Text style={styles.statValue}></Text>
<Text style={styles.statLabel}>Hêvî / Hope</Text>
</View>
</View>
</View>
</View>
{/* Info Banner */}
<View style={styles.infoBanner}>
<Text style={styles.infoBannerIcon}>💡</Text>
<View style={styles.infoBannerContent}>
<Text style={styles.infoBannerText}>
PezkuwiChain - Blockchain'a yekem a netewî ya Kurdan
</Text>
<Text style={styles.infoBannerTextEn}>
PezkuwiChain - The first national blockchain of the Kurds
</Text>
</View>
</View>
{/* Bottom Spacing */}
<View style={{ height: 40 }} />
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
header: {
padding: 20,
paddingTop: 16,
paddingBottom: 24,
flexDirection: 'row',
alignItems: 'center',
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
backButtonText: {
fontSize: 24,
color: KurdistanColors.spi,
fontWeight: 'bold',
},
headerContent: {
flex: 1,
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold',
color: KurdistanColors.spi,
},
headerSubtitle: {
fontSize: 14,
color: KurdistanColors.spi,
opacity: 0.9,
marginTop: 2,
},
content: {
flex: 1,
padding: 16,
},
section: {
marginBottom: 24,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
sectionIconContainer: {
width: 44,
height: 44,
borderRadius: 12,
backgroundColor: KurdistanColors.sor,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
sectionIcon: {
fontSize: 22,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
sectionSubtitle: {
fontSize: 12,
color: '#666',
},
sectionCard: {
backgroundColor: KurdistanColors.spi,
borderRadius: 16,
padding: 16,
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.08)',
elevation: 4,
},
sectionDescription: {
fontSize: 14,
color: '#444',
lineHeight: 20,
marginBottom: 4,
},
sectionDescriptionEn: {
fontSize: 12,
color: '#888',
lineHeight: 18,
marginBottom: 16,
},
mediaList: {
gap: 12,
},
mediaCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F8F9FA',
borderRadius: 12,
padding: 12,
},
mediaIconContainer: {
width: 48,
height: 48,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
mediaIcon: {
fontSize: 24,
},
mediaInfo: {
flex: 1,
marginLeft: 12,
},
mediaName: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.reş,
marginBottom: 2,
},
mediaDescription: {
fontSize: 12,
color: '#666',
},
comingSoonBadge: {
backgroundColor: KurdistanColors.zer,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
comingSoonText: {
fontSize: 10,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
socialGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
gap: 12,
},
socialButton: {
width: '30%',
alignItems: 'center',
padding: 12,
},
socialIconContainer: {
width: 56,
height: 56,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.15)',
elevation: 3,
},
socialIcon: {
fontSize: 28,
},
socialName: {
fontSize: 12,
fontWeight: '500',
color: '#444',
textAlign: 'center',
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
backgroundColor: '#F0F4F8',
borderRadius: 12,
padding: 16,
marginTop: 16,
},
statItem: {
alignItems: 'center',
},
statValue: {
fontSize: 24,
fontWeight: 'bold',
color: KurdistanColors.sor,
},
statLabel: {
fontSize: 11,
color: '#666',
marginTop: 2,
},
statDivider: {
width: 1,
height: 40,
backgroundColor: '#DDD',
},
infoBanner: {
flexDirection: 'row',
backgroundColor: '#E8F5E9',
borderRadius: 12,
padding: 16,
borderLeftWidth: 4,
borderLeftColor: KurdistanColors.kesk,
},
infoBannerIcon: {
fontSize: 24,
marginRight: 12,
},
infoBannerContent: {
flex: 1,
},
infoBannerText: {
fontSize: 14,
color: '#2E7D32',
fontWeight: '500',
lineHeight: 20,
},
infoBannerTextEn: {
fontSize: 12,
color: '#4CAF50',
marginTop: 4,
lineHeight: 18,
},
});
export default KurdMediaScreen;
File diff suppressed because it is too large Load Diff
+962
View File
@@ -0,0 +1,962 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
ScrollView,
StatusBar,
Alert,
ActivityIndicator,
RefreshControl,
Linking,
Modal,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useNavigation } from '@react-navigation/native';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { KurdistanColors } from '../theme/colors';
// Types
interface Course {
id: number;
owner: string;
name: string;
description: string;
content_link: string;
status: 'Active' | 'Archived';
created_at: number;
}
interface Enrollment {
student: string;
course_id: number;
enrolled_at: number;
completed_at: number | null;
points_earned: number;
}
type TabType = 'courses' | 'enrolled' | 'completed';
const PerwerdeScreen: React.FC = () => {
const navigation = useNavigation();
const { selectedAccount, api, isApiReady } = usePezkuwi();
const isConnected = !!selectedAccount;
// State
const [activeTab, setActiveTab] = useState<TabType>('courses');
const [courses, setCourses] = useState<Course[]>([]);
const [myEnrollments, setMyEnrollments] = useState<Enrollment[]>([]);
const [perwerdeScore, setPerwerdeScore] = useState(0);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [selectedCourse, setSelectedCourse] = useState<Course | null>(null);
const [showCourseModal, setShowCourseModal] = useState(false);
const [enrolling, setEnrolling] = useState(false);
// Fetch all courses from blockchain
const fetchCourses = useCallback(async () => {
if (!api || !isApiReady) return;
try {
const entries = await api.query.perwerde.courses.entries();
const courseList: Course[] = [];
for (const [key, value] of entries) {
if (!value.isEmpty) {
const data = value.toJSON() as any;
courseList.push({
id: data.id,
owner: data.owner,
name: decodeText(data.name),
description: decodeText(data.description),
content_link: decodeText(data.contentLink),
status: data.status,
created_at: data.createdAt,
});
}
}
// Sort by id descending (newest first)
courseList.sort((a, b) => b.id - a.id);
setCourses(courseList);
} catch (error) {
console.error('Error fetching courses:', error);
}
}, [api, isApiReady]);
// Fetch user's enrollments
const fetchMyEnrollments = useCallback(async () => {
if (!api || !isApiReady || !selectedAccount) return;
try {
const studentCourses = await api.query.perwerde.studentCourses(selectedAccount.address);
const courseIds = studentCourses.toJSON() as number[];
const enrollmentList: Enrollment[] = [];
let totalPoints = 0;
for (const courseId of courseIds) {
const enrollment = await api.query.perwerde.enrollments([selectedAccount.address, courseId]);
if (!enrollment.isEmpty) {
const data = enrollment.toJSON() as any;
enrollmentList.push({
student: data.student,
course_id: data.courseId,
enrolled_at: data.enrolledAt,
completed_at: data.completedAt,
points_earned: data.pointsEarned,
});
if (data.completedAt) {
totalPoints += data.pointsEarned;
}
}
}
setMyEnrollments(enrollmentList);
setPerwerdeScore(totalPoints);
} catch (error) {
console.error('Error fetching enrollments:', error);
}
}, [api, isApiReady, selectedAccount]);
// Helper to decode bounded vec to string
const decodeText = (data: number[] | string): string => {
if (typeof data === 'string') return data;
try {
return new TextDecoder().decode(new Uint8Array(data));
} catch {
return '';
}
};
// Load data
const loadData = useCallback(async () => {
setLoading(true);
await Promise.all([fetchCourses(), fetchMyEnrollments()]);
setLoading(false);
}, [fetchCourses, fetchMyEnrollments]);
// Refresh handler
const onRefresh = useCallback(async () => {
setRefreshing(true);
await loadData();
setRefreshing(false);
}, [loadData]);
useEffect(() => {
if (isConnected && api && isApiReady) {
loadData();
}
}, [isConnected, api, isApiReady, loadData]);
// Check if user is enrolled in a course
const isEnrolled = (courseId: number): boolean => {
return myEnrollments.some(e => e.course_id === courseId);
};
// Check if course is completed
const isCompleted = (courseId: number): boolean => {
const enrollment = myEnrollments.find(e => e.course_id === courseId);
return enrollment?.completed_at !== null && enrollment?.completed_at !== undefined;
};
// Get enrollment for a course
const getEnrollment = (courseId: number): Enrollment | undefined => {
return myEnrollments.find(e => e.course_id === courseId);
};
// Enroll in course
const handleEnroll = async (courseId: number) => {
if (!api || !selectedAccount) {
Alert.alert('Xeletî / Error', 'Ji kerema xwe berî têketinê wallet ve girêbidin.');
return;
}
setEnrolling(true);
try {
const extrinsic = api.tx.perwerde.enroll(courseId);
await new Promise<void>((resolve, reject) => {
extrinsic.signAndSend(
selectedAccount.address,
{ signer: selectedAccount.signer },
({ status, dispatchError }) => {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
reject(new Error(`${decoded.section}.${decoded.name}`));
} else {
reject(new Error(dispatchError.toString()));
}
return;
}
if (status.isInBlock || status.isFinalized) {
resolve();
}
}
);
});
Alert.alert(
'Serkeftî! / Success!',
'Tu bi serkeftî tev li kursê bûyî!\n\nYou have successfully enrolled in the course!',
[{ text: 'Temam / OK' }]
);
// Refresh data
await loadData();
setShowCourseModal(false);
} catch (error) {
console.error('Enrollment error:', error);
Alert.alert(
'Xeletî / Error',
`Têketin têk çû: ${error instanceof Error ? error.message : 'Unknown error'}`,
[{ text: 'Temam / OK' }]
);
} finally {
setEnrolling(false);
}
};
// Open IPFS content
const openContent = async (ipfsHash: string) => {
const url = ipfsHash.startsWith('http')
? ipfsHash
: `https://ipfs.io/ipfs/${ipfsHash}`;
try {
const canOpen = await Linking.canOpenURL(url);
if (canOpen) {
await Linking.openURL(url);
} else {
Alert.alert('Xeletî / Error', 'Nikarim linkê vebikum.');
}
} catch (error) {
Alert.alert('Xeletî / Error', 'Tiştek xelet çû.');
}
};
// Filter courses based on active tab
const getFilteredCourses = (): Course[] => {
switch (activeTab) {
case 'courses':
return courses.filter(c => c.status === 'Active');
case 'enrolled':
return courses.filter(c => isEnrolled(c.id) && !isCompleted(c.id));
case 'completed':
return courses.filter(c => isCompleted(c.id));
default:
return [];
}
};
// Render tab button
const renderTab = (tab: TabType, label: string, labelKu: string, count: number) => (
<TouchableOpacity
key={tab}
style={[styles.tab, activeTab === tab && styles.tabActive]}
onPress={() => setActiveTab(tab)}
>
<Text style={[styles.tabText, activeTab === tab && styles.tabTextActive]}>
{labelKu}
</Text>
{count > 0 && (
<View style={[styles.tabBadge, activeTab === tab && styles.tabBadgeActive]}>
<Text style={[styles.tabBadgeText, activeTab === tab && styles.tabBadgeTextActive]}>
{count}
</Text>
</View>
)}
</TouchableOpacity>
);
// Render course card
const renderCourseCard = (course: Course) => {
const enrolled = isEnrolled(course.id);
const completed = isCompleted(course.id);
const enrollment = getEnrollment(course.id);
return (
<TouchableOpacity
key={course.id}
style={styles.courseCard}
onPress={() => {
setSelectedCourse(course);
setShowCourseModal(true);
}}
activeOpacity={0.7}
>
<View style={styles.courseHeader}>
<View style={[
styles.courseIcon,
completed && styles.courseIconCompleted,
enrolled && !completed && styles.courseIconEnrolled,
]}>
<Text style={styles.courseIconText}>
{completed ? '✅' : enrolled ? '📖' : '📚'}
</Text>
</View>
<View style={styles.courseInfo}>
<Text style={styles.courseName} numberOfLines={1}>{course.name}</Text>
<Text style={styles.courseId}>Kurs #{course.id}</Text>
</View>
{completed && enrollment && (
<View style={styles.pointsBadge}>
<Text style={styles.pointsText}>+{enrollment.points_earned}</Text>
</View>
)}
</View>
<Text style={styles.courseDescription} numberOfLines={2}>
{course.description}
</Text>
<View style={styles.courseFooter}>
{completed ? (
<Text style={styles.statusCompleted}>Qediya / Completed</Text>
) : enrolled ? (
<Text style={styles.statusEnrolled}>Tev li / Enrolled</Text>
) : (
<Text style={styles.statusAvailable}>Amade / Available</Text>
)}
</View>
</TouchableOpacity>
);
};
// Not connected view
if (!isConnected) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<LinearGradient
colors={[KurdistanColors.zer, '#F59E0B']}
style={styles.notConnectedGradient}
>
<View style={styles.notConnectedContent}>
<Text style={styles.notConnectedIcon}>📚</Text>
<Text style={styles.notConnectedTitle}>Perwerde</Text>
<Text style={styles.notConnectedSubtitle}>
Platforma Perwerdehiya Dijîtal{'\n'}Digital Education Platform
</Text>
<Text style={styles.notConnectedText}>
Ji kerema xwe wallet ve girêbidin da ku bikaribin kursan bibînin û tev li wan bibin.
</Text>
<Text style={styles.notConnectedTextEn}>
Please connect your wallet to view and enroll in courses.
</Text>
</View>
</LinearGradient>
</SafeAreaView>
);
}
const filteredCourses = getFilteredCourses();
const enrolledCount = myEnrollments.filter(e => !e.completed_at).length;
const completedCount = myEnrollments.filter(e => e.completed_at).length;
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
{/* Header */}
<LinearGradient
colors={[KurdistanColors.zer, '#F59E0B']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.header}
>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Text style={styles.backButtonText}></Text>
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.headerTitle}>Perwerde</Text>
<Text style={styles.headerSubtitle}>Platforma Perwerdehiya Dijîtal</Text>
</View>
</LinearGradient>
{/* Score Card */}
<View style={styles.scoreCard}>
<View style={styles.scoreItem}>
<Text style={styles.scoreValue}>{perwerdeScore}</Text>
<Text style={styles.scoreLabel}>Puan / Points</Text>
</View>
<View style={styles.scoreDivider} />
<View style={styles.scoreItem}>
<Text style={styles.scoreValue}>{completedCount}</Text>
<Text style={styles.scoreLabel}>Qediyayî / Done</Text>
</View>
<View style={styles.scoreDivider} />
<View style={styles.scoreItem}>
<Text style={styles.scoreValue}>{enrolledCount}</Text>
<Text style={styles.scoreLabel}>Aktîv / Active</Text>
</View>
</View>
{/* Tabs */}
<View style={styles.tabContainer}>
{renderTab('courses', 'Courses', 'Kurs', courses.filter(c => c.status === 'Active').length)}
{renderTab('enrolled', 'Enrolled', 'Tev li', enrolledCount)}
{renderTab('completed', 'Completed', 'Qediya', completedCount)}
</View>
{/* Content */}
<ScrollView
style={styles.content}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.zer} />
<Text style={styles.loadingText}> barkirin... / Loading...</Text>
</View>
) : filteredCourses.length === 0 ? (
<View style={styles.emptyContainer}>
<Text style={styles.emptyIcon}>
{activeTab === 'courses' ? '📭' : activeTab === 'enrolled' ? '📋' : '🎓'}
</Text>
<Text style={styles.emptyText}>
{activeTab === 'courses'
? 'Kursek tune / No courses available'
: activeTab === 'enrolled'
? 'Tu tev li kursekê nebûyî / Not enrolled in any course'
: 'Kursek neqediyaye / No completed courses'}
</Text>
</View>
) : (
<View style={styles.courseList}>
{filteredCourses.map(renderCourseCard)}
</View>
)}
<View style={{ height: 40 }} />
</ScrollView>
{/* Course Detail Modal */}
<Modal
visible={showCourseModal}
transparent
animationType="slide"
onRequestClose={() => setShowCourseModal(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContainer}>
{selectedCourse && (
<>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>{selectedCourse.name}</Text>
<TouchableOpacity onPress={() => setShowCourseModal(false)}>
<Text style={styles.modalClose}></Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.modalContent}>
<View style={styles.modalSection}>
<Text style={styles.modalSectionTitle}>Danasîn / Description</Text>
<Text style={styles.modalDescription}>{selectedCourse.description}</Text>
</View>
<View style={styles.modalSection}>
<Text style={styles.modalSectionTitle}>Agahdarî / Info</Text>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Kurs ID:</Text>
<Text style={styles.infoValue}>#{selectedCourse.id}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Rewş / Status:</Text>
<Text style={[
styles.infoValue,
{ color: selectedCourse.status === 'Active' ? KurdistanColors.kesk : '#999' }
]}>
{selectedCourse.status === 'Active' ? 'Aktîv' : 'Arşîv'}
</Text>
</View>
{isEnrolled(selectedCourse.id) && (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Têketin / Enrolled:</Text>
<Text style={[styles.infoValue, { color: KurdistanColors.kesk }]}>
{isCompleted(selectedCourse.id) ? 'Qediya ✅' : 'Aktîv 📖'}
</Text>
</View>
)}
{isCompleted(selectedCourse.id) && (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Puan / Points:</Text>
<Text style={[styles.infoValue, { color: KurdistanColors.sor, fontWeight: 'bold' }]}>
+{getEnrollment(selectedCourse.id)?.points_earned || 0}
</Text>
</View>
)}
</View>
{selectedCourse.content_link && (
<TouchableOpacity
style={styles.contentButton}
onPress={() => openContent(selectedCourse.content_link)}
>
<Text style={styles.contentButtonIcon}>📄</Text>
<Text style={styles.contentButtonText}>
Naveroka Kursê Veke / Open Course Content
</Text>
</TouchableOpacity>
)}
</ScrollView>
<View style={styles.modalFooter}>
{!isEnrolled(selectedCourse.id) && selectedCourse.status === 'Active' ? (
<TouchableOpacity
style={styles.enrollButton}
onPress={() => handleEnroll(selectedCourse.id)}
disabled={enrolling}
>
{enrolling ? (
<ActivityIndicator color={KurdistanColors.spi} />
) : (
<>
<Text style={styles.enrollButtonIcon}>📝</Text>
<Text style={styles.enrollButtonText}>Tev li Kursê / Enroll</Text>
</>
)}
</TouchableOpacity>
) : isCompleted(selectedCourse.id) ? (
<View style={styles.completedBanner}>
<Text style={styles.completedBannerIcon}>🎓</Text>
<Text style={styles.completedBannerText}>
Te ev kurs qedand! / You completed this course!
</Text>
</View>
) : (
<View style={styles.enrolledBanner}>
<Text style={styles.enrolledBannerIcon}>📖</Text>
<Text style={styles.enrolledBannerText}>
Tu tev li kursê / You are enrolled
</Text>
</View>
)}
</View>
</>
)}
</View>
</View>
</Modal>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
header: {
padding: 20,
paddingTop: 16,
paddingBottom: 24,
flexDirection: 'row',
alignItems: 'center',
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0, 0, 0, 0.15)',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
backButtonText: {
fontSize: 24,
color: KurdistanColors.reş,
fontWeight: 'bold',
},
headerContent: {
flex: 1,
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
headerSubtitle: {
fontSize: 14,
color: KurdistanColors.reş,
opacity: 0.8,
marginTop: 2,
},
scoreCard: {
flexDirection: 'row',
backgroundColor: KurdistanColors.spi,
marginHorizontal: 16,
marginTop: -12,
borderRadius: 16,
padding: 16,
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
elevation: 6,
},
scoreItem: {
flex: 1,
alignItems: 'center',
},
scoreValue: {
fontSize: 28,
fontWeight: 'bold',
color: KurdistanColors.zer,
},
scoreLabel: {
fontSize: 12,
color: '#666',
marginTop: 4,
},
scoreDivider: {
width: 1,
backgroundColor: '#E0E0E0',
marginVertical: 4,
},
tabContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingTop: 20,
paddingBottom: 8,
gap: 8,
},
tab: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
paddingHorizontal: 12,
borderRadius: 12,
backgroundColor: '#E0E0E0',
gap: 6,
},
tabActive: {
backgroundColor: KurdistanColors.zer,
},
tabText: {
fontSize: 13,
fontWeight: '600',
color: '#666',
},
tabTextActive: {
color: KurdistanColors.reş,
},
tabBadge: {
backgroundColor: '#999',
borderRadius: 10,
paddingHorizontal: 6,
paddingVertical: 2,
minWidth: 20,
alignItems: 'center',
},
tabBadgeActive: {
backgroundColor: KurdistanColors.reş,
},
tabBadgeText: {
fontSize: 10,
fontWeight: 'bold',
color: KurdistanColors.spi,
},
tabBadgeTextActive: {
color: KurdistanColors.zer,
},
content: {
flex: 1,
padding: 16,
},
loadingContainer: {
padding: 40,
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 14,
color: '#666',
},
emptyContainer: {
padding: 60,
alignItems: 'center',
},
emptyIcon: {
fontSize: 64,
marginBottom: 16,
},
emptyText: {
fontSize: 16,
color: '#666',
textAlign: 'center',
},
courseList: {
gap: 12,
},
courseCard: {
backgroundColor: KurdistanColors.spi,
borderRadius: 16,
padding: 16,
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.08)',
elevation: 4,
},
courseHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
courseIcon: {
width: 48,
height: 48,
borderRadius: 12,
backgroundColor: '#F0F0F0',
justifyContent: 'center',
alignItems: 'center',
},
courseIconEnrolled: {
backgroundColor: '#E3F2FD',
},
courseIconCompleted: {
backgroundColor: '#E8F5E9',
},
courseIconText: {
fontSize: 24,
},
courseInfo: {
flex: 1,
marginLeft: 12,
},
courseName: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.reş,
},
courseId: {
fontSize: 12,
color: '#999',
marginTop: 2,
},
pointsBadge: {
backgroundColor: KurdistanColors.kesk,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
pointsText: {
fontSize: 12,
fontWeight: 'bold',
color: KurdistanColors.spi,
},
courseDescription: {
fontSize: 14,
color: '#666',
lineHeight: 20,
marginBottom: 12,
},
courseFooter: {
flexDirection: 'row',
alignItems: 'center',
},
statusAvailable: {
fontSize: 12,
fontWeight: '600',
color: KurdistanColors.yer,
},
statusEnrolled: {
fontSize: 12,
fontWeight: '600',
color: '#1976D2',
},
statusCompleted: {
fontSize: 12,
fontWeight: '600',
color: KurdistanColors.kesk,
},
// Modal styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContainer: {
backgroundColor: KurdistanColors.spi,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
maxHeight: '80%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: KurdistanColors.reş,
flex: 1,
marginRight: 12,
},
modalClose: {
fontSize: 24,
color: '#999',
padding: 4,
},
modalContent: {
padding: 20,
},
modalSection: {
marginBottom: 20,
},
modalSectionTitle: {
fontSize: 14,
fontWeight: '600',
color: '#999',
marginBottom: 8,
textTransform: 'uppercase',
},
modalDescription: {
fontSize: 15,
color: '#444',
lineHeight: 22,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
},
infoLabel: {
fontSize: 14,
color: '#666',
},
infoValue: {
fontSize: 14,
color: KurdistanColors.reş,
fontWeight: '500',
},
contentButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#E3F2FD',
padding: 14,
borderRadius: 12,
gap: 8,
},
contentButtonIcon: {
fontSize: 20,
},
contentButtonText: {
fontSize: 14,
fontWeight: '600',
color: '#1976D2',
},
modalFooter: {
padding: 20,
borderTopWidth: 1,
borderTopColor: '#E0E0E0',
},
enrollButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: KurdistanColors.kesk,
padding: 16,
borderRadius: 12,
gap: 8,
},
enrollButtonIcon: {
fontSize: 20,
},
enrollButtonText: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.spi,
},
completedBanner: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#E8F5E9',
padding: 14,
borderRadius: 12,
gap: 8,
},
completedBannerIcon: {
fontSize: 20,
},
completedBannerText: {
fontSize: 14,
fontWeight: '600',
color: KurdistanColors.kesk,
},
enrolledBanner: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#E3F2FD',
padding: 14,
borderRadius: 12,
gap: 8,
},
enrolledBannerIcon: {
fontSize: 20,
},
enrolledBannerText: {
fontSize: 14,
fontWeight: '600',
color: '#1976D2',
},
// Not connected styles
notConnectedGradient: {
flex: 1,
},
notConnectedContent: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 40,
},
notConnectedIcon: {
fontSize: 80,
marginBottom: 20,
},
notConnectedTitle: {
fontSize: 32,
fontWeight: 'bold',
color: KurdistanColors.reş,
marginBottom: 8,
},
notConnectedSubtitle: {
fontSize: 16,
color: KurdistanColors.reş,
textAlign: 'center',
opacity: 0.8,
marginBottom: 24,
},
notConnectedText: {
fontSize: 14,
color: KurdistanColors.reş,
textAlign: 'center',
lineHeight: 20,
marginBottom: 8,
},
notConnectedTextEn: {
fontSize: 12,
color: KurdistanColors.reş,
textAlign: 'center',
opacity: 0.7,
lineHeight: 18,
},
});
export default PerwerdeScreen;
File diff suppressed because it is too large Load Diff
+193
View File
@@ -0,0 +1,193 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
StatusBar,
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useNavigation } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { fetchUserTikis } from '../../shared/lib/tiki';
import { PezkuwiWebView } from '../components';
/**
* Proposals Screen
*
* Requires Welati (citizen) tiki to access law proposals.
* Uses WebView to load the proposals interface from the web app.
*/
const ProposalsScreen: React.FC = () => {
const navigation = useNavigation();
const { api, isApiReady, selectedAccount } = usePezkuwi();
const [hasWelatiTiki, setHasWelatiTiki] = useState<boolean | null>(null);
const [checkingAccess, setCheckingAccess] = useState(true);
useEffect(() => {
const checkAccess = async () => {
if (!api || !isApiReady || !selectedAccount) {
setCheckingAccess(false);
setHasWelatiTiki(false);
return;
}
try {
const tikis = await fetchUserTikis(api, selectedAccount.address);
const hasWelati = tikis.includes('Welati');
setHasWelatiTiki(hasWelati);
} catch (error) {
if (__DEV__) console.error('[Proposals] Error checking tiki:', error);
setHasWelatiTiki(false);
} finally {
setCheckingAccess(false);
}
};
checkAccess();
}, [api, isApiReady, selectedAccount]);
// Loading state
if (checkingAccess) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<LinearGradient colors={[KurdistanColors.kesk, '#006633']} style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.spi} />
<Text style={styles.loadingText}>Checking access...</Text>
</LinearGradient>
</SafeAreaView>
);
}
// Access denied - no welati tiki
if (!hasWelatiTiki) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<LinearGradient
colors={[KurdistanColors.kesk, KurdistanColors.zer, KurdistanColors.sor]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.accessDeniedContainer}
>
<View style={styles.accessDeniedContent}>
<Text style={styles.accessDeniedIcon}>📜</Text>
<Text style={styles.accessDeniedTitle}>Citizenship Required</Text>
<Text style={styles.accessDeniedSubtitle}>
Pêdivî ye ku hûn welatî bin da ku bikarin pêşniyarên qanûnî bibînin
</Text>
<Text style={styles.accessDeniedText}>
You must be a citizen to view and participate in law proposals.
Please complete your citizenship application first.
</Text>
<TouchableOpacity
style={styles.becomeCitizenButton}
onPress={() => navigation.navigate('BeCitizen' as never)}
>
<Text style={styles.becomeCitizenButtonText}>Become a Citizen</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Text style={styles.backButtonText}> Go Back</Text>
</TouchableOpacity>
</View>
</LinearGradient>
</SafeAreaView>
);
}
// Access granted - show WebView
return (
<SafeAreaView style={styles.container}>
<PezkuwiWebView
path="/proposals"
title="Pêşniyar / Proposals"
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
color: KurdistanColors.spi,
marginTop: 16,
fontSize: 16,
},
accessDeniedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
accessDeniedContent: {
backgroundColor: 'rgba(255,255,255,0.95)',
borderRadius: 24,
padding: 32,
margin: 24,
alignItems: 'center',
boxShadow: '0px 8px 24px rgba(0, 0, 0, 0.2)',
elevation: 10,
},
accessDeniedIcon: {
fontSize: 64,
marginBottom: 16,
},
accessDeniedTitle: {
fontSize: 24,
fontWeight: 'bold',
color: KurdistanColors.reş,
marginBottom: 8,
},
accessDeniedSubtitle: {
fontSize: 14,
color: KurdistanColors.kesk,
textAlign: 'center',
marginBottom: 16,
fontStyle: 'italic',
},
accessDeniedText: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 24,
lineHeight: 22,
},
becomeCitizenButton: {
backgroundColor: KurdistanColors.kesk,
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
marginBottom: 12,
},
becomeCitizenButtonText: {
color: KurdistanColors.spi,
fontSize: 16,
fontWeight: 'bold',
},
backButton: {
padding: 12,
},
backButtonText: {
color: KurdistanColors.kesk,
fontSize: 16,
},
});
export default ProposalsScreen;
+404 -19
View File
@@ -11,8 +11,12 @@ import {
Alert,
Clipboard,
ActivityIndicator,
Modal,
Linking,
Platform,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import QRCode from 'react-native-qrcode-svg';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { KurdistanColors } from '../theme/colors';
import {
@@ -22,6 +26,16 @@ import {
type ReferralStats as BlockchainReferralStats,
} from '../../shared/lib/referral';
// Share platform types
type SharePlatform = 'whatsapp' | 'telegram' | 'viber' | 'email' | 'sms' | 'copy' | 'other';
interface ShareOption {
id: SharePlatform;
name: string;
icon: string;
color: string;
}
interface ReferralStats {
totalReferrals: number;
activeReferrals: number;
@@ -39,6 +53,17 @@ interface Referral {
earned: string;
}
// Share platform options
const SHARE_OPTIONS: ShareOption[] = [
{ id: 'whatsapp', name: 'WhatsApp', icon: '💬', color: '#25D366' },
{ id: 'telegram', name: 'Telegram', icon: '✈️', color: '#0088CC' },
{ id: 'viber', name: 'Viber', icon: '📱', color: '#665CAC' },
{ id: 'email', name: 'Email', icon: '📧', color: '#EA4335' },
{ id: 'sms', name: 'SMS', icon: '💬', color: '#34B7F1' },
{ id: 'copy', name: 'Copy Link', icon: '📋', color: '#6B7280' },
{ id: 'other', name: 'Other', icon: '📤', color: '#9CA3AF' },
];
const ReferralScreen: React.FC = () => {
const { selectedAccount, api, connectWallet, isApiReady } = usePezkuwi();
const isConnected = !!selectedAccount;
@@ -54,12 +79,27 @@ const ReferralScreen: React.FC = () => {
});
const [referrals, setReferrals] = useState<Referral[]>([]);
const [loading, setLoading] = useState(false);
const [showShareSheet, setShowShareSheet] = useState(false);
const [showQRModal, setShowQRModal] = useState(false);
// Generate referral code from wallet address
// Generate referral code and link from wallet address
const referralCode = selectedAccount
? `PZK-${selectedAccount.address.slice(0, 8).toUpperCase()}`
: 'PZK-CONNECT-WALLET';
// Full referral link for sharing
const referralLink = selectedAccount
? `https://pezkuwi.app/join?ref=${selectedAccount.address}`
: '';
// Deep link for app-to-app sharing
const deepLink = selectedAccount
? `pezkuwi://join?ref=${selectedAccount.address}`
: '';
// Pre-formatted share message
const shareMessage = `🌟 Pezkuwi'ye Katıl! / Join Pezkuwi!\n\nDijital Kurdistan vatandaşı ol ve ödüller kazan!\nBecome a Digital Kurdistan citizen and earn rewards!\n\n🔗 ${referralLink}\n\n#Pezkuwi #Kurdistan #Web3`;
// Fetch referral data from blockchain
const fetchReferralData = useCallback(async () => {
if (!api || !isApiReady || !selectedAccount) {
@@ -131,24 +171,90 @@ const ReferralScreen: React.FC = () => {
}
};
const handleCopyCode = () => {
Clipboard.setString(referralCode);
Alert.alert('Copied!', 'Referral code copied to clipboard');
const handleCopyLink = () => {
Clipboard.setString(referralLink);
Alert.alert('Kopyalandı! / Copied!', 'Referral linki kopyalandı / Referral link copied to clipboard');
setShowShareSheet(false);
};
const handleShareCode = async () => {
try {
const result = await Share.share({
message: `Join Pezkuwi using my referral code: ${referralCode}\n\nGet rewards for becoming a citizen!`,
title: 'Join Pezkuwi',
});
const handleShareViaPlatform = async (platform: SharePlatform) => {
if (!selectedAccount) return;
if (result.action === Share.sharedAction) {
if (__DEV__) console.warn('Shared successfully');
const encodedMessage = encodeURIComponent(shareMessage);
const encodedLink = encodeURIComponent(referralLink);
let url = '';
switch (platform) {
case 'whatsapp':
url = `whatsapp://send?text=${encodedMessage}`;
break;
case 'telegram':
url = `tg://msg?text=${encodedMessage}`;
break;
case 'viber':
url = `viber://forward?text=${encodedMessage}`;
break;
case 'email':
url = `mailto:?subject=${encodeURIComponent('Pezkuwi\'ye Katıl! / Join Pezkuwi!')}&body=${encodedMessage}`;
break;
case 'sms':
url = Platform.OS === 'ios'
? `sms:&body=${encodedMessage}`
: `sms:?body=${encodedMessage}`;
break;
case 'copy':
handleCopyLink();
return;
case 'other':
default:
// Use system share sheet
try {
await Share.share({
message: shareMessage,
title: 'Pezkuwi\'ye Katıl! / Join Pezkuwi!',
});
} catch (error) {
if (__DEV__) console.error('Error sharing:', error);
}
setShowShareSheet(false);
return;
}
// Try to open the platform-specific URL
try {
const canOpen = await Linking.canOpenURL(url);
if (canOpen) {
await Linking.openURL(url);
} else {
// Fallback to system share if app not installed
Alert.alert(
'Uygulama Bulunamadı / App Not Found',
`${SHARE_OPTIONS.find(o => o.id === platform)?.name} yüklü değil. Diğer paylaşım yöntemini deneyin.\n\n${SHARE_OPTIONS.find(o => o.id === platform)?.name} is not installed. Try another sharing method.`,
[
{ text: 'Tamam / OK' },
{
text: 'Diğer / Other',
onPress: () => handleShareViaPlatform('other')
},
]
);
}
} catch (error) {
if (__DEV__) console.error('Error sharing:', error);
if (__DEV__) console.error('Error opening URL:', error);
// Fallback to system share
handleShareViaPlatform('other');
}
setShowShareSheet(false);
};
const openShareSheet = () => {
setShowShareSheet(true);
};
const openQRModal = () => {
setShowQRModal(true);
};
if (!isConnected) {
@@ -211,18 +317,18 @@ const ReferralScreen: React.FC = () => {
</View>
<View style={styles.codeActions}>
<TouchableOpacity
style={[styles.codeButton, styles.copyButton]}
onPress={handleCopyCode}
style={[styles.codeButton, styles.qrButton]}
onPress={openQRModal}
>
<Text style={styles.codeButtonIcon}>📋</Text>
<Text style={styles.codeButtonText}>Copy</Text>
<Text style={styles.codeButtonIcon}>📱</Text>
<Text style={styles.codeButtonText}>QR Code</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.codeButton, styles.shareButton]}
onPress={handleShareCode}
onPress={openShareSheet}
>
<Text style={styles.codeButtonIcon}>📤</Text>
<Text style={styles.codeButtonText}>Share</Text>
<Text style={[styles.codeButtonText, { color: KurdistanColors.spi }]}>Paylaş / Share</Text>
</TouchableOpacity>
</View>
</View>
@@ -430,6 +536,117 @@ const ReferralScreen: React.FC = () => {
)}
</View>
</ScrollView>
{/* Share Sheet Modal */}
<Modal
visible={showShareSheet}
transparent
animationType="slide"
onRequestClose={() => setShowShareSheet(false)}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={() => setShowShareSheet(false)}
>
<View style={styles.shareSheetContainer}>
<View style={styles.shareSheetHandle} />
<Text style={styles.shareSheetTitle}>Paylaş / Share</Text>
<Text style={styles.shareSheetSubtitle}>
Arkadaşlarını davet et, ödül kazan!{'\n'}Invite friends, earn rewards!
</Text>
{/* Share Link Preview */}
<View style={styles.linkPreview}>
<Text style={styles.linkPreviewText} numberOfLines={1}>
{referralLink}
</Text>
</View>
{/* Platform Grid */}
<View style={styles.platformGrid}>
{SHARE_OPTIONS.map((option) => (
<TouchableOpacity
key={option.id}
style={styles.platformButton}
onPress={() => handleShareViaPlatform(option.id)}
activeOpacity={0.7}
>
<View style={[styles.platformIconContainer, { backgroundColor: option.color }]}>
<Text style={styles.platformIcon}>{option.icon}</Text>
</View>
<Text style={styles.platformName}>{option.name}</Text>
</TouchableOpacity>
))}
</View>
{/* Cancel Button */}
<TouchableOpacity
style={styles.cancelButton}
onPress={() => setShowShareSheet(false)}
>
<Text style={styles.cancelButtonText}>İptal / Cancel</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
</Modal>
{/* QR Code Modal */}
<Modal
visible={showQRModal}
transparent
animationType="fade"
onRequestClose={() => setShowQRModal(false)}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={() => setShowQRModal(false)}
>
<View style={styles.qrModalContainer}>
<View style={styles.qrModalHeader}>
<Text style={styles.qrModalTitle}>Referral QR Kodu</Text>
<TouchableOpacity onPress={() => setShowQRModal(false)}>
<Text style={styles.qrCloseButton}></Text>
</TouchableOpacity>
</View>
<View style={styles.qrCodeWrapper}>
{selectedAccount && (
<QRCode
value={referralLink}
size={200}
backgroundColor={KurdistanColors.spi}
color={KurdistanColors.reş}
/>
)}
</View>
<Text style={styles.qrInstructions}>
Bu QR kodu arkadaşlarınla paylaş.{'\n'}
Taratarak Pezkuwi'ye katılabilirler.
</Text>
<Text style={styles.qrInstructionsEn}>
Share this QR code with friends.{'\n'}
They can scan to join Pezkuwi.
</Text>
<View style={styles.qrCodeContainer}>
<Text style={styles.qrCodeText}>{referralCode}</Text>
</View>
<TouchableOpacity
style={styles.qrShareButton}
onPress={() => {
setShowQRModal(false);
openShareSheet();
}}
>
<Text style={styles.qrShareButtonText}>📤 Linki Paylaş / Share Link</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
</Modal>
</SafeAreaView>
);
};
@@ -819,6 +1036,174 @@ const styles = StyleSheet.create({
color: KurdistanColors.kesk,
fontWeight: '600',
},
qrButton: {
backgroundColor: '#F0F0F0',
},
// Share Sheet Modal Styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
shareSheetContainer: {
backgroundColor: KurdistanColors.spi,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
padding: 20,
paddingBottom: 40,
},
shareSheetHandle: {
width: 40,
height: 4,
backgroundColor: '#E0E0E0',
borderRadius: 2,
alignSelf: 'center',
marginBottom: 16,
},
shareSheetTitle: {
fontSize: 20,
fontWeight: 'bold',
color: KurdistanColors.reş,
textAlign: 'center',
marginBottom: 4,
},
shareSheetSubtitle: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 20,
lineHeight: 20,
},
linkPreview: {
backgroundColor: '#F5F5F5',
borderRadius: 12,
padding: 12,
marginBottom: 20,
},
linkPreviewText: {
fontSize: 12,
color: '#666',
fontFamily: 'monospace',
textAlign: 'center',
},
platformGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
marginBottom: 20,
},
platformButton: {
width: '25%',
alignItems: 'center',
marginBottom: 16,
},
platformIconContainer: {
width: 56,
height: 56,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
elevation: 3,
},
platformIcon: {
fontSize: 28,
},
platformName: {
fontSize: 12,
color: '#666',
fontWeight: '500',
},
cancelButton: {
backgroundColor: '#F0F0F0',
borderRadius: 12,
padding: 16,
alignItems: 'center',
},
cancelButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#666',
},
// QR Modal Styles
qrModalContainer: {
backgroundColor: KurdistanColors.spi,
borderRadius: 24,
margin: 20,
padding: 24,
alignItems: 'center',
alignSelf: 'center',
marginTop: 'auto',
marginBottom: 'auto',
boxShadow: '0px 4px 20px rgba(0, 0, 0, 0.3)',
elevation: 10,
},
qrModalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
marginBottom: 20,
},
qrModalTitle: {
fontSize: 18,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
qrCloseButton: {
fontSize: 20,
color: '#999',
padding: 4,
},
qrCodeWrapper: {
padding: 20,
backgroundColor: KurdistanColors.spi,
borderRadius: 16,
borderWidth: 2,
borderColor: KurdistanColors.sor,
marginBottom: 16,
},
qrInstructions: {
fontSize: 14,
color: '#666',
textAlign: 'center',
lineHeight: 20,
marginBottom: 8,
},
qrInstructionsEn: {
fontSize: 12,
color: '#999',
textAlign: 'center',
lineHeight: 18,
marginBottom: 16,
},
qrCodeContainer: {
backgroundColor: '#F5F5F5',
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 8,
marginBottom: 20,
},
qrCodeText: {
fontSize: 16,
fontWeight: 'bold',
color: KurdistanColors.sor,
fontFamily: 'monospace',
},
qrShareButton: {
backgroundColor: KurdistanColors.sor,
borderRadius: 12,
paddingHorizontal: 24,
paddingVertical: 14,
width: '100%',
alignItems: 'center',
},
qrShareButtonText: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.spi,
},
});
export default ReferralScreen;
+870
View File
@@ -0,0 +1,870 @@
import React, { useState, useMemo } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
ScrollView,
StatusBar,
TextInput,
Alert,
ActivityIndicator,
Modal,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { KurdistanColors } from '../theme/colors';
type ContributionType = 'zekat' | 'tax';
interface AllocationItem {
id: string;
nameKu: string;
nameEn: string;
icon: string;
percentage: number;
}
const DEFAULT_ALLOCATIONS: AllocationItem[] = [
{ id: 'shahid', nameKu: 'Binemalin Şehîda', nameEn: 'Martyr Families', icon: '🏠', percentage: 0 },
{ id: 'education', nameKu: 'Projeyin Perwerde', nameEn: 'Education Projects', icon: '📚', percentage: 0 },
{ id: 'health', nameKu: 'Tenduristî', nameEn: 'Health Services', icon: '🏥', percentage: 0 },
{ id: 'orphans', nameKu: 'Sêwî û Feqîr', nameEn: 'Orphans & Poor', icon: '👶', percentage: 0 },
{ id: 'infrastructure', nameKu: 'Binesazî', nameEn: 'Infrastructure', icon: '🏗️', percentage: 0 },
{ id: 'defense', nameKu: 'Parastina Welat', nameEn: 'National Defense', icon: '🛡️', percentage: 0 },
{ id: 'diaspora', nameKu: 'Diaspora', nameEn: 'Diaspora Support', icon: '🌍', percentage: 0 },
{ id: 'culture', nameKu: 'Çand û Huner', nameEn: 'Culture & Arts', icon: '🎭', percentage: 0 },
];
const TaxZekatScreen: React.FC = () => {
const navigation = useNavigation();
const { api, selectedAccount, getKeyPair } = usePezkuwi();
const [contributionType, setContributionType] = useState<ContributionType>('zekat');
const [amount, setAmount] = useState('');
const [allocations, setAllocations] = useState<AllocationItem[]>(DEFAULT_ALLOCATIONS);
const [termsAccepted, setTermsAccepted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
// Calculate total percentage
const totalPercentage = useMemo(() => {
return allocations.reduce((sum, item) => sum + item.percentage, 0);
}, [allocations]);
// Check if form is valid
const isFormValid = useMemo(() => {
const amountNum = parseFloat(amount);
return (
amountNum > 0 &&
totalPercentage === 100 &&
termsAccepted &&
selectedAccount
);
}, [amount, totalPercentage, termsAccepted, selectedAccount]);
// Update allocation percentage
const updateAllocation = (id: string, value: string) => {
const numValue = parseInt(value) || 0;
const clampedValue = Math.min(100, Math.max(0, numValue));
setAllocations(prev =>
prev.map(item =>
item.id === id ? { ...item, percentage: clampedValue } : item
)
);
};
// Calculate HEZ amount for each allocation
const calculateAllocationAmount = (percentage: number): string => {
const amountNum = parseFloat(amount) || 0;
return ((amountNum * percentage) / 100).toFixed(2);
};
// Handle submit
const handleSubmit = async () => {
if (!isFormValid) {
if (totalPercentage !== 100) {
Alert.alert('Şaşî / Error', 'Dabeşkirin divê %100 be / Allocation must equal 100%');
} else if (!termsAccepted) {
Alert.alert('Şaşî / Error', 'Divê hûn şertnameyê qebûl bikin / You must accept the terms');
}
return;
}
setShowConfirmModal(true);
};
// Confirm and send transaction to Treasury
const confirmAndSend = async () => {
setShowConfirmModal(false);
setIsSubmitting(true);
try {
if (!api || !selectedAccount) {
throw new Error('Wallet not connected');
}
const keyPair = await getKeyPair(selectedAccount.address);
if (!keyPair) {
throw new Error('Could not retrieve key pair');
}
// Prepare allocation data as remark
const allocationData = allocations
.filter(a => a.percentage > 0)
.map(a => `${a.id}:${a.percentage}`)
.join(',');
// Create remark message with contribution details
const remarkMessage = JSON.stringify({
type: contributionType,
allocations: allocationData,
timestamp: Date.now(),
});
const amountInUnits = BigInt(Math.floor(parseFloat(amount) * 1e12)); // Convert to smallest unit (12 decimals for HEZ)
// Get treasury account address
// Treasury account is derived from pallet ID "py/trsry" (standard Substrate treasury)
const treasuryAccount = api.consts.treasury?.palletId
? api.registry.createType('AccountId', api.consts.treasury.palletId.toU8a())
: null;
if (!treasuryAccount) {
throw new Error('Treasury account not found');
}
if (__DEV__) {
console.log('[TaxZekat] Treasury account:', treasuryAccount.toString());
console.log('[TaxZekat] Amount:', amountInUnits.toString());
console.log('[TaxZekat] Remark:', remarkMessage);
}
// Batch: Transfer to treasury + Remark with allocation data
const txs = [
api.tx.balances.transferKeepAlive(treasuryAccount, amountInUnits.toString()),
api.tx.system.remark(remarkMessage),
];
// Submit batch transaction
await new Promise<void>((resolve, reject) => {
api.tx.utility
.batch(txs)
.signAndSend(keyPair, { nonce: -1 }, ({ status, dispatchError }) => {
if (status.isInBlock || status.isFinalized) {
if (dispatchError) {
let errorMessage = 'Transaction failed';
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}`;
}
reject(new Error(errorMessage));
return;
}
resolve();
}
})
.catch(reject);
});
Alert.alert(
'Serketî / Success',
`${contributionType === 'zekat' ? 'Zekat' : 'Bac'} bi serkeftî hat şandin!\n\nMiqdar: ${amount} HEZ\n\nSpas ji bo beşdariya we!\nThank you for your contribution!`,
[{ text: 'Temam / OK', onPress: () => navigation.goBack() }]
);
} catch (error) {
Alert.alert(
'Şaşî / Error',
error instanceof Error ? error.message : 'An error occurred'
);
} finally {
setIsSubmitting(false);
}
};
// Render allocation item
const renderAllocationItem = (item: AllocationItem) => (
<View key={item.id} style={styles.allocationItem}>
<View style={styles.allocationInfo}>
<Text style={styles.allocationIcon}>{item.icon}</Text>
<View style={styles.allocationText}>
<Text style={styles.allocationName}>{item.nameKu}</Text>
<Text style={styles.allocationNameEn}>{item.nameEn}</Text>
</View>
</View>
<View style={styles.allocationInput}>
<TextInput
style={styles.percentageInput}
value={item.percentage > 0 ? String(item.percentage) : ''}
onChangeText={(value) => updateAllocation(item.id, value)}
keyboardType="number-pad"
placeholder="0"
placeholderTextColor="#999"
maxLength={3}
/>
<Text style={styles.percentSign}>%</Text>
</View>
{item.percentage > 0 && parseFloat(amount) > 0 && (
<Text style={styles.allocationHez}>
{calculateAllocationAmount(item.percentage)} HEZ
</Text>
)}
</View>
);
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
<Text style={styles.backButtonText}> Paş</Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>Bac û Zekat</Text>
<Text style={styles.headerSubtitle}>Tax & Zekat</Text>
</View>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
{/* Description */}
<View style={styles.descriptionBox}>
<Text style={styles.descriptionText}>
Beşdariya xwe ya bi dilxwazî ji Komara Dijitaliya Kurdistanê re bişînin.
</Text>
<Text style={styles.descriptionTextEn}>
Send your voluntary contribution to the Digital Kurdistan Republic.
</Text>
</View>
{/* Type Selection */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Cureyê Beşdariyê / Contribution Type</Text>
<View style={styles.typeSelector}>
<TouchableOpacity
style={[
styles.typeButton,
contributionType === 'zekat' && styles.typeButtonActive,
]}
onPress={() => setContributionType('zekat')}
>
<Text style={styles.typeIcon}></Text>
<Text style={[
styles.typeText,
contributionType === 'zekat' && styles.typeTextActive,
]}>
Zekat
</Text>
<Text style={[
styles.typeSubtext,
contributionType === 'zekat' && styles.typeSubtextActive,
]}>
İslami Zekat
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.typeButton,
contributionType === 'tax' && styles.typeButtonActive,
]}
onPress={() => setContributionType('tax')}
>
<Text style={styles.typeIcon}>📜</Text>
<Text style={[
styles.typeText,
contributionType === 'tax' && styles.typeTextActive,
]}>
Bac
</Text>
<Text style={[
styles.typeSubtext,
contributionType === 'tax' && styles.typeSubtextActive,
]}>
Vergi / Tax
</Text>
</TouchableOpacity>
</View>
</View>
{/* Amount Input */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Miqdar / Amount</Text>
<View style={styles.amountContainer}>
<TextInput
style={styles.amountInput}
value={amount}
onChangeText={setAmount}
keyboardType="decimal-pad"
placeholder="0.00"
placeholderTextColor="#999"
/>
<Text style={styles.amountCurrency}>HEZ</Text>
</View>
{selectedAccount && (
<Text style={styles.balanceText}>
Bakiye / Balance: -- HEZ
</Text>
)}
</View>
{/* Allocation Section */}
<View style={styles.section}>
<View style={styles.allocationHeader}>
<Text style={styles.sectionTitle}>Dabeşkirina Fonê / Fund Allocation</Text>
<View style={[
styles.totalBadge,
totalPercentage === 100 && styles.totalBadgeValid,
totalPercentage > 100 && styles.totalBadgeInvalid,
]}>
<Text style={[
styles.totalText,
totalPercentage === 100 && styles.totalTextValid,
totalPercentage > 100 && styles.totalTextInvalid,
]}>
{totalPercentage}%
</Text>
</View>
</View>
<Text style={styles.allocationHint}>
Divê bêkêmasî %100 be / Must equal exactly 100%
</Text>
<View style={styles.allocationList}>
{allocations.map(renderAllocationItem)}
</View>
</View>
{/* Terms Section */}
<View style={styles.termsSection}>
<View style={[
styles.termsBox,
contributionType === 'zekat' ? styles.termsBoxZekat : styles.termsBoxTax,
]}>
<Text style={styles.termsIcon}>
{contributionType === 'zekat' ? '☪️' : '📜'}
</Text>
<Text style={styles.termsTitle}>SOZNAME / COMMITMENT</Text>
{contributionType === 'zekat' ? (
<>
<Text style={styles.termsText}>
Komara Dijitaliya Kurdistanê (Dijital Kurdistan Devleti), İslami usul ve kurallara uygun olarak, zekat gönderimlerinizi TAM OLARAK sizin belirlediğiniz oranlara göre sarfedeceğini TAAHHÜT EDER.
</Text>
<Text style={styles.termsText}>
Zekat fonları yalnızca Kuran'da belirtilen 8 sınıfa (Tevbe 9:60) harcanacaktır: Fakirler, Miskinler, Zekat memurları, Müellefe-i kulub, Köleler, Borçlular, Fi sebilillah, İbn-i sebil.
</Text>
</>
) : (
<>
<Text style={styles.termsText}>
Komara Dijitaliya Kurdistanê (Dijital Kurdistan Devleti), vergi katkılarınızı belirlediğiniz oranlara MÜMKÜN OLDUĞU KADAR uygun şekilde kullanacağını taahhüt eder.
</Text>
<Text style={styles.termsText}>
Acil durumlar veya zorunlu hallerde, devlet küçük inisiyatifler kullanabilir. Tüm harcamalar blockchain üzerinde şeffaf olarak kaydedilecektir.
</Text>
</>
)}
</View>
<TouchableOpacity
style={styles.checkboxContainer}
onPress={() => setTermsAccepted(!termsAccepted)}
>
<View style={[styles.checkbox, termsAccepted && styles.checkboxChecked]}>
{termsAccepted && <Text style={styles.checkmark}>✓</Text>}
</View>
<Text style={styles.checkboxLabel}>
Okudum ve kabul ediyorum / I have read and accept
</Text>
</TouchableOpacity>
</View>
{/* Submit Button */}
<TouchableOpacity
style={[
styles.submitButton,
!isFormValid && styles.submitButtonDisabled,
contributionType === 'zekat' ? styles.submitButtonZekat : styles.submitButtonTax,
]}
onPress={handleSubmit}
disabled={!isFormValid || isSubmitting}
>
{isSubmitting ? (
<ActivityIndicator color="#FFF" />
) : (
<Text style={styles.submitButtonText}>
{contributionType === 'zekat' ? ' ZEKAT BIŞÎNE' : '📤 BAC BIŞÎNE'}
</Text>
)}
</TouchableOpacity>
<View style={{ height: 40 }} />
</ScrollView>
{/* Confirmation Modal */}
<Modal
visible={showConfirmModal}
transparent
animationType="fade"
onRequestClose={() => setShowConfirmModal(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}>Piştrast bike / Confirm</Text>
<View style={styles.modalSummary}>
<View style={styles.modalRow}>
<Text style={styles.modalLabel}>Cure / Type:</Text>
<Text style={styles.modalValue}>
{contributionType === 'zekat' ? 'Zekat' : 'Bac / Tax'}
</Text>
</View>
<View style={styles.modalRow}>
<Text style={styles.modalLabel}>Miqdar / Amount:</Text>
<Text style={styles.modalValue}>{amount} HEZ</Text>
</View>
<Text style={styles.modalSectionTitle}>Dabeşkirin / Allocation:</Text>
{allocations
.filter(a => a.percentage > 0)
.map(a => (
<View key={a.id} style={styles.modalAllocation}>
<Text style={styles.modalAllocationText}>
{a.icon} {a.nameKu}
</Text>
<Text style={styles.modalAllocationValue}>
{calculateAllocationAmount(a.percentage)} HEZ ({a.percentage}%)
</Text>
</View>
))}
</View>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.modalCancelButton}
onPress={() => setShowConfirmModal(false)}
>
<Text style={styles.modalCancelText}>Betal / Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.modalConfirmButton,
contributionType === 'zekat' ? styles.submitButtonZekat : styles.submitButtonTax,
]}
onPress={confirmAndSend}
>
<Text style={styles.modalConfirmText}>✓ Piştrast</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
header: {
backgroundColor: KurdistanColors.spi,
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
backButton: {
marginBottom: 8,
},
backButtonText: {
fontSize: 16,
color: KurdistanColors.kesk,
fontWeight: '600',
},
headerTitle: {
fontSize: 24,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
headerSubtitle: {
fontSize: 14,
color: '#666',
marginTop: 2,
},
content: {
flex: 1,
padding: 16,
},
descriptionBox: {
backgroundColor: `${KurdistanColors.kesk}15`,
borderRadius: 12,
padding: 16,
marginBottom: 16,
borderLeftWidth: 4,
borderLeftColor: KurdistanColors.kesk,
},
descriptionText: {
fontSize: 14,
color: KurdistanColors.reş,
lineHeight: 20,
marginBottom: 8,
},
descriptionTextEn: {
fontSize: 13,
color: '#666',
fontStyle: 'italic',
},
section: {
backgroundColor: KurdistanColors.spi,
borderRadius: 12,
padding: 16,
marginBottom: 16,
},
sectionTitle: {
fontSize: 16,
fontWeight: '700',
color: KurdistanColors.reş,
marginBottom: 12,
},
typeSelector: {
flexDirection: 'row',
gap: 12,
},
typeButton: {
flex: 1,
backgroundColor: '#F8F9FA',
borderRadius: 12,
padding: 16,
alignItems: 'center',
borderWidth: 2,
borderColor: 'transparent',
},
typeButtonActive: {
borderColor: KurdistanColors.kesk,
backgroundColor: `${KurdistanColors.kesk}10`,
},
typeIcon: {
fontSize: 32,
marginBottom: 8,
},
typeText: {
fontSize: 18,
fontWeight: '700',
color: '#333',
},
typeTextActive: {
color: KurdistanColors.kesk,
},
typeSubtext: {
fontSize: 12,
color: '#666',
marginTop: 4,
},
typeSubtextActive: {
color: KurdistanColors.kesk,
},
amountContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F8F9FA',
borderRadius: 12,
borderWidth: 1,
borderColor: '#E0E0E0',
},
amountInput: {
flex: 1,
fontSize: 24,
fontWeight: '600',
padding: 16,
color: KurdistanColors.reş,
},
amountCurrency: {
fontSize: 18,
fontWeight: '700',
color: KurdistanColors.kesk,
paddingRight: 16,
},
balanceText: {
fontSize: 12,
color: '#666',
marginTop: 8,
textAlign: 'right',
},
allocationHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
totalBadge: {
backgroundColor: '#F0F0F0',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
},
totalBadgeValid: {
backgroundColor: `${KurdistanColors.kesk}20`,
},
totalBadgeInvalid: {
backgroundColor: `${KurdistanColors.sor}20`,
},
totalText: {
fontSize: 14,
fontWeight: '700',
color: '#666',
},
totalTextValid: {
color: KurdistanColors.kesk,
},
totalTextInvalid: {
color: KurdistanColors.sor,
},
allocationHint: {
fontSize: 12,
color: '#666',
marginBottom: 16,
},
allocationList: {
gap: 12,
},
allocationItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F8F9FA',
borderRadius: 10,
padding: 12,
},
allocationInfo: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
allocationIcon: {
fontSize: 24,
marginRight: 12,
},
allocationText: {
flex: 1,
},
allocationName: {
fontSize: 14,
fontWeight: '600',
color: KurdistanColors.reş,
},
allocationNameEn: {
fontSize: 11,
color: '#666',
},
allocationInput: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: KurdistanColors.spi,
borderRadius: 8,
borderWidth: 1,
borderColor: '#E0E0E0',
paddingHorizontal: 8,
},
percentageInput: {
width: 40,
fontSize: 16,
fontWeight: '600',
textAlign: 'center',
paddingVertical: 8,
color: KurdistanColors.reş,
},
percentSign: {
fontSize: 14,
color: '#666',
},
allocationHez: {
fontSize: 11,
color: KurdistanColors.kesk,
fontWeight: '600',
marginLeft: 8,
minWidth: 60,
textAlign: 'right',
},
termsSection: {
marginBottom: 16,
},
termsBox: {
borderRadius: 12,
padding: 16,
marginBottom: 12,
},
termsBoxZekat: {
backgroundColor: `${KurdistanColors.kesk}10`,
borderWidth: 1,
borderColor: `${KurdistanColors.kesk}30`,
},
termsBoxTax: {
backgroundColor: `${KurdistanColors.zer}15`,
borderWidth: 1,
borderColor: `${KurdistanColors.zer}40`,
},
termsIcon: {
fontSize: 32,
textAlign: 'center',
marginBottom: 12,
},
termsTitle: {
fontSize: 16,
fontWeight: '700',
color: KurdistanColors.reş,
textAlign: 'center',
marginBottom: 12,
},
termsText: {
fontSize: 13,
color: '#444',
lineHeight: 20,
marginBottom: 12,
textAlign: 'justify',
},
checkboxContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: KurdistanColors.spi,
borderRadius: 12,
padding: 16,
},
checkbox: {
width: 24,
height: 24,
borderRadius: 6,
borderWidth: 2,
borderColor: KurdistanColors.kesk,
marginRight: 12,
justifyContent: 'center',
alignItems: 'center',
},
checkboxChecked: {
backgroundColor: KurdistanColors.kesk,
},
checkmark: {
color: KurdistanColors.spi,
fontSize: 16,
fontWeight: 'bold',
},
checkboxLabel: {
flex: 1,
fontSize: 14,
color: KurdistanColors.reş,
},
submitButton: {
borderRadius: 12,
padding: 18,
alignItems: 'center',
},
submitButtonZekat: {
backgroundColor: KurdistanColors.kesk,
},
submitButtonTax: {
backgroundColor: '#D4A017',
},
submitButtonDisabled: {
opacity: 0.5,
},
submitButtonText: {
fontSize: 18,
fontWeight: 'bold',
color: KurdistanColors.spi,
},
// Modal Styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
modalContent: {
backgroundColor: KurdistanColors.spi,
borderRadius: 16,
padding: 24,
width: '100%',
maxWidth: 400,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: KurdistanColors.reş,
textAlign: 'center',
marginBottom: 20,
},
modalSummary: {
backgroundColor: '#F8F9FA',
borderRadius: 12,
padding: 16,
marginBottom: 20,
},
modalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
modalLabel: {
fontSize: 14,
color: '#666',
},
modalValue: {
fontSize: 14,
fontWeight: '600',
color: KurdistanColors.reş,
},
modalSectionTitle: {
fontSize: 14,
fontWeight: '600',
color: KurdistanColors.reş,
marginTop: 8,
marginBottom: 12,
},
modalAllocation: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 6,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
modalAllocationText: {
fontSize: 13,
color: '#444',
},
modalAllocationValue: {
fontSize: 13,
fontWeight: '600',
color: KurdistanColors.kesk,
},
modalButtons: {
flexDirection: 'row',
gap: 12,
},
modalCancelButton: {
flex: 1,
backgroundColor: '#F0F0F0',
borderRadius: 10,
padding: 14,
alignItems: 'center',
},
modalCancelText: {
fontSize: 16,
fontWeight: '600',
color: '#666',
},
modalConfirmButton: {
flex: 1,
borderRadius: 10,
padding: 14,
alignItems: 'center',
},
modalConfirmText: {
fontSize: 16,
fontWeight: 'bold',
color: KurdistanColors.spi,
},
});
export default TaxZekatScreen;
+193
View File
@@ -0,0 +1,193 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
StatusBar,
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useNavigation } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { fetchUserTikis } from '../../shared/lib/tiki';
import { PezkuwiWebView } from '../components';
/**
* Validators Screen
*
* Requires Welati (citizen) tiki to access validator features.
* Uses WebView to load the validators interface from the web app.
*/
const ValidatorsScreen: React.FC = () => {
const navigation = useNavigation();
const { api, isApiReady, selectedAccount } = usePezkuwi();
const [hasWelatiTiki, setHasWelatiTiki] = useState<boolean | null>(null);
const [checkingAccess, setCheckingAccess] = useState(true);
useEffect(() => {
const checkAccess = async () => {
if (!api || !isApiReady || !selectedAccount) {
setCheckingAccess(false);
setHasWelatiTiki(false);
return;
}
try {
const tikis = await fetchUserTikis(api, selectedAccount.address);
const hasWelati = tikis.includes('Welati');
setHasWelatiTiki(hasWelati);
} catch (error) {
if (__DEV__) console.error('[Validators] Error checking tiki:', error);
setHasWelatiTiki(false);
} finally {
setCheckingAccess(false);
}
};
checkAccess();
}, [api, isApiReady, selectedAccount]);
// Loading state
if (checkingAccess) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<LinearGradient colors={[KurdistanColors.kesk, '#006633']} style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.spi} />
<Text style={styles.loadingText}>Checking access...</Text>
</LinearGradient>
</SafeAreaView>
);
}
// Access denied - no welati tiki
if (!hasWelatiTiki) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<LinearGradient
colors={[KurdistanColors.kesk, KurdistanColors.zer, KurdistanColors.sor]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.accessDeniedContainer}
>
<View style={styles.accessDeniedContent}>
<Text style={styles.accessDeniedIcon}>🛡</Text>
<Text style={styles.accessDeniedTitle}>Citizenship Required</Text>
<Text style={styles.accessDeniedSubtitle}>
Pêdivî ye ku hûn welatî bin da ku bikarin rastkerên torê bibînin
</Text>
<Text style={styles.accessDeniedText}>
You must be a citizen to access network validators.
Please complete your citizenship application first.
</Text>
<TouchableOpacity
style={styles.becomeCitizenButton}
onPress={() => navigation.navigate('BeCitizen' as never)}
>
<Text style={styles.becomeCitizenButtonText}>Become a Citizen</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Text style={styles.backButtonText}> Go Back</Text>
</TouchableOpacity>
</View>
</LinearGradient>
</SafeAreaView>
);
}
// Access granted - show WebView
return (
<SafeAreaView style={styles.container}>
<PezkuwiWebView
path="/validators"
title="Rastker / Validators"
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
color: KurdistanColors.spi,
marginTop: 16,
fontSize: 16,
},
accessDeniedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
accessDeniedContent: {
backgroundColor: 'rgba(255,255,255,0.95)',
borderRadius: 24,
padding: 32,
margin: 24,
alignItems: 'center',
boxShadow: '0px 8px 24px rgba(0, 0, 0, 0.2)',
elevation: 10,
},
accessDeniedIcon: {
fontSize: 64,
marginBottom: 16,
},
accessDeniedTitle: {
fontSize: 24,
fontWeight: 'bold',
color: KurdistanColors.reş,
marginBottom: 8,
},
accessDeniedSubtitle: {
fontSize: 14,
color: KurdistanColors.kesk,
textAlign: 'center',
marginBottom: 16,
fontStyle: 'italic',
},
accessDeniedText: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 24,
lineHeight: 22,
},
becomeCitizenButton: {
backgroundColor: KurdistanColors.kesk,
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
marginBottom: 12,
},
becomeCitizenButtonText: {
color: KurdistanColors.spi,
fontSize: 16,
fontWeight: 'bold',
},
backButton: {
padding: 12,
},
backButtonText: {
color: KurdistanColors.kesk,
fontSize: 16,
},
});
export default ValidatorsScreen;
+193
View File
@@ -0,0 +1,193 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
StatusBar,
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useNavigation } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { fetchUserTikis } from '../../shared/lib/tiki';
import { PezkuwiWebView } from '../components';
/**
* Vote Screen
*
* Requires Welati (citizen) tiki to access voting features.
* Uses WebView to load the voting interface from the web app.
*/
const VoteScreen: React.FC = () => {
const navigation = useNavigation();
const { api, isApiReady, selectedAccount } = usePezkuwi();
const [hasWelatiTiki, setHasWelatiTiki] = useState<boolean | null>(null);
const [checkingAccess, setCheckingAccess] = useState(true);
useEffect(() => {
const checkAccess = async () => {
if (!api || !isApiReady || !selectedAccount) {
setCheckingAccess(false);
setHasWelatiTiki(false);
return;
}
try {
const tikis = await fetchUserTikis(api, selectedAccount.address);
const hasWelati = tikis.includes('Welati');
setHasWelatiTiki(hasWelati);
} catch (error) {
if (__DEV__) console.error('[Vote] Error checking tiki:', error);
setHasWelatiTiki(false);
} finally {
setCheckingAccess(false);
}
};
checkAccess();
}, [api, isApiReady, selectedAccount]);
// Loading state
if (checkingAccess) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<LinearGradient colors={[KurdistanColors.kesk, '#006633']} style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.spi} />
<Text style={styles.loadingText}>Checking access...</Text>
</LinearGradient>
</SafeAreaView>
);
}
// Access denied - no welati tiki
if (!hasWelatiTiki) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<LinearGradient
colors={[KurdistanColors.kesk, KurdistanColors.zer, KurdistanColors.sor]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.accessDeniedContainer}
>
<View style={styles.accessDeniedContent}>
<Text style={styles.accessDeniedIcon}>🗳</Text>
<Text style={styles.accessDeniedTitle}>Citizenship Required</Text>
<Text style={styles.accessDeniedSubtitle}>
Pêdivî ye ku hûn welatî bin da ku bikarin deng bidin
</Text>
<Text style={styles.accessDeniedText}>
You must be a citizen to participate in voting.
Please complete your citizenship application first.
</Text>
<TouchableOpacity
style={styles.becomeCitizenButton}
onPress={() => navigation.navigate('BeCitizen' as never)}
>
<Text style={styles.becomeCitizenButtonText}>Become a Citizen</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Text style={styles.backButtonText}> Go Back</Text>
</TouchableOpacity>
</View>
</LinearGradient>
</SafeAreaView>
);
}
// Access granted - show WebView
return (
<SafeAreaView style={styles.container}>
<PezkuwiWebView
path="/vote"
title="Dengdan / Vote"
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
color: KurdistanColors.spi,
marginTop: 16,
fontSize: 16,
},
accessDeniedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
accessDeniedContent: {
backgroundColor: 'rgba(255,255,255,0.95)',
borderRadius: 24,
padding: 32,
margin: 24,
alignItems: 'center',
boxShadow: '0px 8px 24px rgba(0, 0, 0, 0.2)',
elevation: 10,
},
accessDeniedIcon: {
fontSize: 64,
marginBottom: 16,
},
accessDeniedTitle: {
fontSize: 24,
fontWeight: 'bold',
color: KurdistanColors.reş,
marginBottom: 8,
},
accessDeniedSubtitle: {
fontSize: 14,
color: KurdistanColors.kesk,
textAlign: 'center',
marginBottom: 16,
fontStyle: 'italic',
},
accessDeniedText: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 24,
lineHeight: 22,
},
becomeCitizenButton: {
backgroundColor: KurdistanColors.kesk,
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
marginBottom: 12,
},
becomeCitizenButtonText: {
color: KurdistanColors.spi,
fontSize: 16,
fontWeight: 'bold',
},
backButton: {
padding: 12,
},
backButtonText: {
color: KurdistanColors.kesk,
fontSize: 16,
},
});
export default VoteScreen;
@@ -4,419 +4,228 @@ exports[`BeCitizenScreen should match snapshot 1`] = `
<RCTSafeAreaView
style={
{
"backgroundColor": "#F5F5F5",
"backgroundColor": "#FFFFFF",
"flex": 1,
}
}
>
<LinearGradient
colors={
[
"#00A94F",
"#FFD700",
"#EE2A35",
]
}
end={
{
"x": 1,
"y": 1,
}
}
start={
{
"x": 0,
"y": 0,
}
}
<View
style={
{
"backgroundColor": "#FFFFFF",
"flex": 1,
}
}
>
<RCTScrollView
contentContainerStyle={
<View
style={
{
"flexGrow": 1,
"padding": 20,
"paddingTop": 60,
"alignItems": "center",
"backgroundColor": "#FFFFFF",
"borderBottomColor": "#E0E0E0",
"borderBottomWidth": 1,
"flexDirection": "row",
"justifyContent": "space-between",
"paddingHorizontal": 16,
"paddingVertical": 12,
}
}
showsVerticalScrollIndicator={false}
>
<View>
<View
<Text
style={
{
"color": "#000",
"flex": 1,
"fontSize": 18,
"fontWeight": "700",
"textAlign": "center",
}
}
>
Be Citizen
</Text>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
{
"opacity": 1,
"padding": 8,
}
}
>
<Text
style={
{
"alignItems": "center",
"marginBottom": 40,
"color": "#00A94F",
"fontSize": 14,
"fontWeight": "600",
}
}
>
<View
style={
{
"alignItems": "center",
"backgroundColor": "#FFFFFF",
"borderRadius": 50,
"boxShadow": "0px 4px 8px rgba(0, 0, 0, 0.3)",
"elevation": 8,
"height": 100,
"justifyContent": "center",
"marginBottom": 20,
"width": 100,
}
}
>
<Text
style={
{
"fontSize": 48,
}
}
>
🏛️
</Text>
</View>
<Text
style={
{
"color": "#FFFFFF",
"fontSize": 28,
"fontWeight": "bold",
"marginBottom": 8,
}
}
>
Be a Citizen
</Text>
<Text
style={
{
"color": "#FFFFFF",
"fontSize": 16,
"opacity": 0.9,
"textAlign": "center",
}
}
>
Join the Pezkuwi decentralized nation
</Text>
</View>
<View
style={
{
"gap": 16,
"marginBottom": 40,
}
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
{
"alignItems": "center",
"backgroundColor": "#FFFFFF",
"borderRadius": 20,
"boxShadow": "0px 4px 8px rgba(0, 0, 0, 0.2)",
"elevation": 6,
"opacity": 1,
"padding": 24,
}
}
>
<Text
style={
{
"fontSize": 48,
"marginBottom": 16,
}
}
>
📝
</Text>
<Text
style={
{
"color": "#00A94F",
"fontSize": 20,
"fontWeight": "bold",
"marginBottom": 8,
}
}
>
New Citizen
</Text>
<Text
style={
{
"color": "#666",
"fontSize": 14,
"textAlign": "center",
}
}
>
Apply for citizenship and join our community
</Text>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
{
"alignItems": "center",
"backgroundColor": "#FFFFFF",
"borderRadius": 20,
"boxShadow": "0px 4px 8px rgba(0, 0, 0, 0.2)",
"elevation": 6,
"opacity": 1,
"padding": 24,
}
}
>
<Text
style={
{
"fontSize": 48,
"marginBottom": 16,
}
}
>
🔐
</Text>
<Text
style={
{
"color": "#00A94F",
"fontSize": 20,
"fontWeight": "bold",
"marginBottom": 8,
}
}
>
Existing Citizen
</Text>
<Text
style={
{
"color": "#666",
"fontSize": 14,
"textAlign": "center",
}
}
>
Access your citizenship account
</Text>
</View>
</View>
<View
style={
{
"backgroundColor": "rgba(255, 255, 255, 0.2)",
"borderRadius": 16,
"padding": 20,
}
}
>
<Text
style={
{
"color": "#FFFFFF",
"fontSize": 18,
"fontWeight": "600",
"marginBottom": 16,
}
}
>
Citizenship Benefits
</Text>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"marginBottom": 12,
}
}
>
<Text
style={
{
"color": "#FFFFFF",
"fontSize": 16,
"fontWeight": "bold",
"marginRight": 12,
}
}
>
</Text>
<Text
style={
{
"color": "#FFFFFF",
"flex": 1,
"fontSize": 14,
}
}
>
Voting rights in governance
</Text>
</View>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"marginBottom": 12,
}
}
>
<Text
style={
{
"color": "#FFFFFF",
"fontSize": 16,
"fontWeight": "bold",
"marginRight": 12,
}
}
>
</Text>
<Text
style={
{
"color": "#FFFFFF",
"flex": 1,
"fontSize": 14,
}
}
>
Access to exclusive services
</Text>
</View>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"marginBottom": 12,
}
}
>
<Text
style={
{
"color": "#FFFFFF",
"fontSize": 16,
"fontWeight": "bold",
"marginRight": 12,
}
}
>
</Text>
<Text
style={
{
"color": "#FFFFFF",
"flex": 1,
"fontSize": 14,
}
}
>
Referral rewards program
</Text>
</View>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"marginBottom": 12,
}
}
>
<Text
style={
{
"color": "#FFFFFF",
"fontSize": 16,
"fontWeight": "bold",
"marginRight": 12,
}
}
>
</Text>
<Text
style={
{
"color": "#FFFFFF",
"flex": 1,
"fontSize": 14,
}
}
>
Community recognition
</Text>
</View>
</View>
Reload
</Text>
</View>
</RCTScrollView>
</LinearGradient>
</View>
<WebView
allowsBackForwardNavigationGestures={true}
allowsInlineMediaPlayback={true}
bounces={true}
cacheEnabled={true}
cacheMode="LOAD_DEFAULT"
domStorageEnabled={true}
injectedJavaScript="
(function() {
// Mark this as mobile app
window.PEZKUWI_MOBILE = true;
window.PEZKUWI_PLATFORM = 'ios';
// Inject wallet address if connected
window.PEZKUWI_ADDRESS = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
window.PEZKUWI_ACCOUNT_NAME = 'Mobile Wallet';
// Override console.log to send to React Native (for debugging)
const originalConsoleLog = console.log;
console.log = function(...args) {
originalConsoleLog.apply(console, args);
window.ReactNativeWebView?.postMessage(JSON.stringify({
type: 'CONSOLE_LOG',
payload: args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')
}));
};
// Create native bridge for wallet operations
window.PezkuwiNativeBridge = {
// Request transaction signing and submission from native wallet
signTransaction: function(payload, callback) {
window.__pendingSignCallback = callback;
window.ReactNativeWebView?.postMessage(JSON.stringify({
type: 'SIGN_TRANSACTION',
payload: payload
}));
},
// Request wallet connection
connectWallet: function() {
window.ReactNativeWebView?.postMessage(JSON.stringify({
type: 'CONNECT_WALLET'
}));
},
// Navigate back in native app
goBack: function() {
window.ReactNativeWebView?.postMessage(JSON.stringify({
type: 'GO_BACK'
}));
},
// Check if wallet is connected
isWalletConnected: function() {
return !!window.PEZKUWI_ADDRESS;
},
// Get connected address
getAddress: function() {
return window.PEZKUWI_ADDRESS || null;
}
};
// Notify web app that native bridge is ready
window.dispatchEvent(new CustomEvent('pezkuwi-native-ready', {
detail: {
address: window.PEZKUWI_ADDRESS,
platform: window.PEZKUWI_PLATFORM
}
}));
true; // Required for injectedJavaScript
})();
"
javaScriptEnabled={true}
mediaPlaybackRequiresUserAction={false}
onError={[Function]}
onHttpError={[Function]}
onLoadEnd={[Function]}
onLoadStart={[Function]}
onMessage={[Function]}
onNavigationStateChange={[Function]}
pullToRefreshEnabled={true}
ref={
{
"current": null,
}
}
sharedCookiesEnabled={true}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={true}
source={
{
"uri": "https://pezkuwichain.io/be-citizen",
}
}
style={
{
"flex": 1,
}
}
thirdPartyCookiesEnabled={true}
webviewDebuggingEnabled={true}
/>
<View
style={
{
"alignItems": "center",
"backgroundColor": "rgba(255, 255, 255, 0.9)",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<ActivityIndicator
color="#00A94F"
size="large"
/>
<Text
style={
{
"color": "#666",
"fontSize": 14,
"marginTop": 12,
}
}
>
Loading...
</Text>
</View>
</View>
</RCTSafeAreaView>
`;
@@ -1531,10 +1531,7 @@ exports[`DashboardScreen should match snapshot 1`] = `
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
false,
]
}
>
@@ -1547,26 +1544,6 @@ exports[`DashboardScreen should match snapshot 1`] = `
>
📊
</Text>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
@@ -1580,7 +1557,7 @@ exports[`DashboardScreen should match snapshot 1`] = `
}
}
>
Tax
Bac/Zekat
</Text>
</View>
<View
@@ -1791,10 +1768,7 @@ exports[`DashboardScreen should match snapshot 1`] = `
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
false,
]
}
>
@@ -1807,26 +1781,6 @@ exports[`DashboardScreen should match snapshot 1`] = `
>
👑
</Text>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
@@ -2003,10 +1957,7 @@ exports[`DashboardScreen should match snapshot 1`] = `
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
false,
]
}
>
@@ -2019,26 +1970,6 @@ exports[`DashboardScreen should match snapshot 1`] = `
>
🗳️
</Text>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
@@ -2106,10 +2037,7 @@ exports[`DashboardScreen should match snapshot 1`] = `
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
false,
]
}
>
@@ -2122,26 +2050,6 @@ exports[`DashboardScreen should match snapshot 1`] = `
>
🛡️
</Text>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
@@ -2312,10 +2220,7 @@ exports[`DashboardScreen should match snapshot 1`] = `
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
false,
]
}
>
@@ -2328,26 +2233,6 @@ exports[`DashboardScreen should match snapshot 1`] = `
>
📜
</Text>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
@@ -2841,10 +2726,7 @@ exports[`DashboardScreen should match snapshot 1`] = `
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
false,
]
}
>
@@ -2863,26 +2745,6 @@ exports[`DashboardScreen should match snapshot 1`] = `
}
}
/>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
@@ -3640,315 +3502,6 @@ exports[`DashboardScreen should match snapshot 1`] = `
Perwerde
</Text>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
{
"alignItems": "center",
"marginBottom": 16,
"opacity": 1,
"width": "25%",
}
}
>
<View
style={
[
{
"alignItems": "center",
"backgroundColor": "#F8F9FA",
"borderRadius": 16,
"boxShadow": "0px 1px 2px rgba(0, 0, 0, 0.05)",
"elevation": 1,
"height": 56,
"justifyContent": "center",
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
]
}
>
<Text
style={
{
"fontSize": 28,
}
}
>
📜
</Text>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
style={
{
"color": "#333",
"fontSize": 11,
"fontWeight": "500",
"maxWidth": "90%",
"textAlign": "center",
}
}
>
Library
</Text>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
{
"alignItems": "center",
"marginBottom": 16,
"opacity": 1,
"width": "25%",
}
}
>
<View
style={
[
{
"alignItems": "center",
"backgroundColor": "#F8F9FA",
"borderRadius": 16,
"boxShadow": "0px 1px 2px rgba(0, 0, 0, 0.05)",
"elevation": 1,
"height": 56,
"justifyContent": "center",
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
]
}
>
<Text
style={
{
"fontSize": 28,
}
}
>
🗣️
</Text>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
style={
{
"color": "#333",
"fontSize": 11,
"fontWeight": "500",
"maxWidth": "90%",
"textAlign": "center",
}
}
>
Language
</Text>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
{
"alignItems": "center",
"marginBottom": 16,
"opacity": 1,
"width": "25%",
}
}
>
<View
style={
[
{
"alignItems": "center",
"backgroundColor": "#F8F9FA",
"borderRadius": 16,
"boxShadow": "0px 1px 2px rgba(0, 0, 0, 0.05)",
"elevation": 1,
"height": 56,
"justifyContent": "center",
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
]
}
>
<Text
style={
{
"fontSize": 28,
}
}
>
🧸
</Text>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
style={
{
"color": "#333",
"fontSize": 11,
"fontWeight": "500",
"maxWidth": "90%",
"textAlign": "center",
}
}
>
Kids
</Text>
</View>
<View
accessibilityState={
{
@@ -4155,109 +3708,6 @@ exports[`DashboardScreen should match snapshot 1`] = `
Research
</Text>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
{
"alignItems": "center",
"marginBottom": 16,
"opacity": 1,
"width": "25%",
}
}
>
<View
style={
[
{
"alignItems": "center",
"backgroundColor": "#F8F9FA",
"borderRadius": 16,
"boxShadow": "0px 1px 2px rgba(0, 0, 0, 0.05)",
"elevation": 1,
"height": 56,
"justifyContent": "center",
"marginBottom": 6,
"width": 56,
},
{
"backgroundColor": "#F0F0F0",
"opacity": 0.5,
},
]
}
>
<Text
style={
{
"fontSize": 28,
}
}
>
🏺
</Text>
<View
style={
{
"backgroundColor": "transparent",
"position": "absolute",
"right": -4,
"top": -4,
}
}
>
<Text
style={
{
"fontSize": 12,
}
}
>
🔒
</Text>
</View>
</View>
<Text
numberOfLines={1}
style={
{
"color": "#333",
"fontSize": 11,
"fontWeight": "500",
"maxWidth": "90%",
"textAlign": "center",
}
}
>
History
</Text>
</View>
</View>
</View>
<View
@@ -173,7 +173,7 @@ exports[`ReferralScreen should match snapshot 1`] = `
}
}
>
📋
📱
</Text>
<Text
style={
@@ -184,7 +184,7 @@ exports[`ReferralScreen should match snapshot 1`] = `
}
}
>
Copy
QR Code
</Text>
</View>
<View
@@ -240,14 +240,19 @@ exports[`ReferralScreen should match snapshot 1`] = `
</Text>
<Text
style={
{
"color": "#000000",
"fontSize": 14,
"fontWeight": "600",
}
[
{
"color": "#000000",
"fontSize": 14,
"fontWeight": "600",
},
{
"color": "#FFFFFF",
},
]
}
>
Share
Paylaş / Share
</Text>
</View>
</View>
+112
View File
@@ -0,0 +1,112 @@
/**
* Mobile-specific citizenship workflow utilities
* Uses native keyPair signing instead of browser extension
*/
import type { ApiPromise } from '@pezkuwi/api';
import type { KeyringPair } from '@pezkuwi/keyring/types';
export interface CitizenshipResult {
success: boolean;
error?: string;
blockHash?: string;
}
/**
* Submit KYC application using native keyPair (mobile)
*/
export async function submitKycApplicationMobile(
api: ApiPromise,
keyPair: KeyringPair,
name: string,
email: string,
ipfsCid: string,
notes: string = 'Citizenship application via mobile'
): Promise<CitizenshipResult> {
try {
if (!api?.tx?.identityKyc?.setIdentity || !api?.tx?.identityKyc?.applyForKyc) {
return { success: false, error: 'Identity KYC pallet not available' };
}
const address = keyPair.address;
// Check if user already has a pending KYC application
const pendingApp = await api.query.identityKyc.pendingKycApplications(address);
if (!pendingApp.isEmpty) {
return {
success: false,
error: 'You already have a pending citizenship application. Please wait for approval.'
};
}
// Check if user is already approved
const kycStatus = await api.query.identityKyc.kycStatuses(address);
if (kycStatus.toString() === 'Approved') {
return {
success: false,
error: 'Your citizenship application is already approved!'
};
}
const cidString = String(ipfsCid);
if (!cidString || cidString === 'undefined' || cidString === '[object Object]') {
return { success: false, error: `Invalid IPFS CID: ${cidString}` };
}
if (__DEV__) {
console.log('[Citizenship] Submitting for:', address);
}
// Step 1: Set identity
const identityResult = await new Promise<CitizenshipResult>((resolve) => {
api.tx.identityKyc
.setIdentity(name, email)
.signAndSend(keyPair, { nonce: -1 }, ({ status, dispatchError }) => {
if (status.isInBlock || status.isFinalized) {
if (dispatchError) {
let errorMessage = 'Identity transaction failed';
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}`;
}
resolve({ success: false, error: errorMessage });
return;
}
resolve({ success: true });
}
})
.catch((error) => resolve({ success: false, error: error.message }));
});
if (!identityResult.success) {
return identityResult;
}
// Step 2: Apply for KYC
const kycResult = await new Promise<CitizenshipResult>((resolve) => {
api.tx.identityKyc
.applyForKyc(cidString, notes)
.signAndSend(keyPair, { nonce: -1 }, ({ status, dispatchError }) => {
if (status.isInBlock || status.isFinalized) {
if (dispatchError) {
let errorMessage = 'KYC application failed';
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}`;
}
resolve({ success: false, error: errorMessage });
return;
}
const blockHash = status.isFinalized
? status.asFinalized?.toString()
: status.asInBlock?.toString();
resolve({ success: true, blockHash });
}
})
.catch((error) => resolve({ success: false, error: error.message }));
});
return kycResult;
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
}