mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-21 23:47:56 +00:00
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:
+9
-1
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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'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'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',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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}>Tê 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 vê kursê yî / 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
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user