From f89fd43855ead647ce1881bfefe1ab817ccfd974 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Fri, 16 Jan 2026 19:41:23 +0300 Subject: [PATCH] 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 --- mobile/app.json | 10 +- mobile/eas.json | 9 +- mobile/metro.config.cjs | 8 + mobile/package.json | 2 +- mobile/src/components/PezkuwiWebView.tsx | 46 +- mobile/src/lib/supabase.ts | 17 +- mobile/src/navigation/AppNavigator.tsx | 84 + mobile/src/navigation/BottomTabNavigator.tsx | 2 +- mobile/src/screens/B2BScreen.tsx | 1428 +++++++++++++++++ mobile/src/screens/BeCitizenApplyScreen.tsx | 21 +- mobile/src/screens/BeCitizenScreen.tsx | 590 +------ mobile/src/screens/DashboardScreen.tsx | 86 +- mobile/src/screens/IdentityScreen.tsx | 1112 +++++++++++++ mobile/src/screens/KurdMediaScreen.tsx | 543 +++++++ mobile/src/screens/LaunchpadScreen.tsx | 1183 ++++++++++++++ mobile/src/screens/PerwerdeScreen.tsx | 962 +++++++++++ mobile/src/screens/PresidentScreen.tsx | 1368 ++++++++++++++++ mobile/src/screens/ProposalsScreen.tsx | 193 +++ mobile/src/screens/ReferralScreen.tsx | 423 ++++- mobile/src/screens/TaxZekatScreen.tsx | 870 ++++++++++ mobile/src/screens/ValidatorsScreen.tsx | 193 +++ mobile/src/screens/VoteScreen.tsx | 193 +++ .../BeCitizenScreen.test.tsx.snap | 597 +++---- .../DashboardScreen.test.tsx.snap | 564 +------ .../ReferralScreen.test.tsx.snap | 21 +- mobile/src/utils/citizenship.ts | 112 ++ 26 files changed, 9032 insertions(+), 1605 deletions(-) create mode 100644 mobile/src/screens/B2BScreen.tsx create mode 100644 mobile/src/screens/IdentityScreen.tsx create mode 100644 mobile/src/screens/KurdMediaScreen.tsx create mode 100644 mobile/src/screens/LaunchpadScreen.tsx create mode 100644 mobile/src/screens/PerwerdeScreen.tsx create mode 100644 mobile/src/screens/PresidentScreen.tsx create mode 100644 mobile/src/screens/ProposalsScreen.tsx create mode 100644 mobile/src/screens/TaxZekatScreen.tsx create mode 100644 mobile/src/screens/ValidatorsScreen.tsx create mode 100644 mobile/src/screens/VoteScreen.tsx create mode 100644 mobile/src/utils/citizenship.ts diff --git a/mobile/app.json b/mobile/app.json index 8ee32743..766375de 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -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" + } } } } diff --git a/mobile/eas.json b/mobile/eas.json index 66d91018..27b4fb86 100644 --- a/mobile/eas.json +++ b/mobile/eas.json @@ -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" } } }, diff --git a/mobile/metro.config.cjs b/mobile/metro.config.cjs index b013aa49..25d01fc3 100644 --- a/mobile/metro.config.cjs +++ b/mobile/metro.config.cjs @@ -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; diff --git a/mobile/package.json b/mobile/package.json index 77e23cda..65ccfcb2 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -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", diff --git a/mobile/src/components/PezkuwiWebView.tsx b/mobile/src/components/PezkuwiWebView.tsx index ce3bffa2..0b98838d 100644 --- a/mobile/src/components/PezkuwiWebView.tsx +++ b/mobile/src/components/PezkuwiWebView.tsx @@ -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 = ({ const [error, setError] = useState(null); const [canGoBack, setCanGoBack] = useState(false); + const navigation = useNavigation>(); const { selectedAccount, getKeyPair, api, isApiReady } = usePezkuwi(); // JavaScript to inject into the WebView @@ -214,9 +221,40 @@ const PezkuwiWebView: React.FC = ({ 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 = ({ console.error('Failed to parse WebView message:', parseError); } } - }, [selectedAccount, getKeyPair, canGoBack]); + }, [selectedAccount, getKeyPair, canGoBack, navigation, api, isApiReady]); // Handle Android back button useFocusEffect( diff --git a/mobile/src/lib/supabase.ts b/mobile/src/lib/supabase.ts index 52b9a4d5..433d2371 100644 --- a/mobile/src/lib/supabase.ts +++ b/mobile/src/lib/supabase.ts @@ -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, { diff --git a/mobile/src/navigation/AppNavigator.tsx b/mobile/src/navigation/AppNavigator.tsx index 8f409bb5..780812b5 100644 --- a/mobile/src/navigation/AppNavigator.tsx +++ b/mobile/src/navigation/AppNavigator.tsx @@ -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(); @@ -155,6 +179,66 @@ const AppNavigator: React.FC = () => { headerShown: false, }} /> + + + + + + + + + + + + )} diff --git a/mobile/src/navigation/BottomTabNavigator.tsx b/mobile/src/navigation/BottomTabNavigator.tsx index a208d478..5559be49 100644 --- a/mobile/src/navigation/BottomTabNavigator.tsx +++ b/mobile/src/navigation/BottomTabNavigator.tsx @@ -65,7 +65,7 @@ const BottomTabNavigator: React.FC = () => { name="Home" component={DashboardScreen} options={{ - header: (props) => , + headerShown: false, tabBarLabel: 'Home', tabBarIcon: ({ color, focused }) => ( diff --git a/mobile/src/screens/B2BScreen.tsx b/mobile/src/screens/B2BScreen.tsx new file mode 100644 index 00000000..02fe68a6 --- /dev/null +++ b/mobile/src/screens/B2BScreen.tsx @@ -0,0 +1,1428 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + SafeAreaView, + ScrollView, + StatusBar, + TextInput, + Alert, + ActivityIndicator, + Modal, + FlatList, + Image, + RefreshControl, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { usePezkuwi } from '../contexts/PezkuwiContext'; +import { KurdistanColors } from '../theme/colors'; + +// Types +interface Business { + id: string; + walletAddress: string; + name: string; + description: string; + region: string; + rating: number; + reviewCount: number; + verified: boolean; + createdAt: number; +} + +interface Listing { + id: string; + businessId: string; + business: Business; + title: string; + description: string; + category: CategoryType; + price: number; + currency: 'HEZ' | 'PEZ' | 'USDT'; + unit: string; + minOrder: number; + images: string[]; + location: string; + isService: boolean; + createdAt: number; + status: 'active' | 'sold' | 'paused'; +} + +type CategoryType = + | 'agriculture' + | 'manufacturing' + | 'technology' + | 'logistics' + | 'construction' + | 'food' + | 'textiles' + | 'services'; + +interface Category { + id: CategoryType; + nameKu: string; + nameEn: string; + icon: string; +} + +const CATEGORIES: Category[] = [ + { id: 'agriculture', nameKu: 'Çandinî', nameEn: 'Agriculture', icon: '🌾' }, + { id: 'manufacturing', nameKu: 'Pîşesazî', nameEn: 'Manufacturing', icon: '🏭' }, + { id: 'technology', nameKu: 'Teknolojî', nameEn: 'Technology', icon: '💻' }, + { id: 'logistics', nameKu: 'Veguhaztin', nameEn: 'Logistics', icon: '🚛' }, + { id: 'construction', nameKu: 'Avahîsazî', nameEn: 'Construction', icon: '🏗️' }, + { id: 'food', nameKu: 'Xwarin', nameEn: 'Food & Beverage', icon: '🍽️' }, + { id: 'textiles', nameKu: 'Tekstîl', nameEn: 'Textiles', icon: '🧵' }, + { id: 'services', nameKu: 'Xizmet', nameEn: 'Services', icon: '💼' }, +]; + +// Mock data - will be replaced with Supabase/blockchain +const MOCK_LISTINGS: Listing[] = [ + { + id: '1', + businessId: 'b1', + business: { + id: 'b1', + walletAddress: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + name: 'Agro Kurdistan Co.', + description: 'Leading agricultural exports', + region: 'Hewlêr', + rating: 4.8, + reviewCount: 45, + verified: true, + createdAt: Date.now() - 86400000 * 30, + }, + title: 'Premium Kurdish Wheat - Export Quality', + description: 'High-quality wheat from the fertile lands of Kurdistan. Organic farming methods. Bulk orders available.', + category: 'agriculture', + price: 500, + currency: 'HEZ', + unit: 'ton', + minOrder: 10, + images: [], + location: 'Hewlêr, Başûr', + isService: false, + createdAt: Date.now() - 86400000 * 5, + status: 'active', + }, + { + id: '2', + businessId: 'b2', + business: { + id: 'b2', + walletAddress: '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', + name: 'TechKurd Solutions', + description: 'Software development & IT services', + region: 'Diaspora', + rating: 4.9, + reviewCount: 128, + verified: true, + createdAt: Date.now() - 86400000 * 60, + }, + title: 'Full-Stack Web Development', + description: 'Professional web application development. React, Node.js, Blockchain integration. Kurdish & English support.', + category: 'technology', + price: 5000, + currency: 'HEZ', + unit: 'project', + minOrder: 1, + images: [], + location: 'Diaspora (Remote)', + isService: true, + createdAt: Date.now() - 86400000 * 2, + status: 'active', + }, + { + id: '3', + businessId: 'b3', + business: { + id: 'b3', + walletAddress: '5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy', + name: 'Kurdistan Textiles', + description: 'Traditional & modern textile production', + region: 'Amed', + rating: 4.6, + reviewCount: 67, + verified: true, + createdAt: Date.now() - 86400000 * 90, + }, + title: 'Traditional Kurdish Kilim - Wholesale', + description: 'Handwoven Kurdish kilims with traditional patterns. Perfect for export. Custom designs available.', + category: 'textiles', + price: 200, + currency: 'HEZ', + unit: 'piece', + minOrder: 50, + images: [], + location: 'Amed, Bakur', + isService: false, + createdAt: Date.now() - 86400000 * 1, + status: 'active', + }, + { + id: '4', + businessId: 'b4', + business: { + id: 'b4', + walletAddress: '5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw', + name: 'Kurdistan Logistics', + description: 'Cross-border shipping & logistics', + region: 'Silêmanî', + rating: 4.7, + reviewCount: 89, + verified: true, + createdAt: Date.now() - 86400000 * 45, + }, + title: 'International Shipping - Kurdistan to Europe', + description: 'Reliable shipping from Kurdistan to European countries. Full documentation support. Crypto payments accepted.', + category: 'logistics', + price: 150, + currency: 'HEZ', + unit: 'shipment', + minOrder: 1, + images: [], + location: 'Silêmanî, Başûr', + isService: true, + createdAt: Date.now() - 86400000 * 3, + status: 'active', + }, +]; + +const B2BScreen: React.FC = () => { + const navigation = useNavigation(); + const { selectedAccount, api, getKeyPair } = usePezkuwi(); + + // State + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('all'); + const [listings, setListings] = useState(MOCK_LISTINGS); + const [refreshing, setRefreshing] = useState(false); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showDetailModal, setShowDetailModal] = useState(false); + const [selectedListing, setSelectedListing] = useState(null); + const [showContactModal, setShowContactModal] = useState(false); + + // Create listing form state + const [newListing, setNewListing] = useState({ + title: '', + description: '', + category: 'services' as CategoryType, + price: '', + unit: '', + minOrder: '1', + location: '', + isService: false, + }); + + // Contact form state + const [contactMessage, setContactMessage] = useState(''); + const [offerAmount, setOfferAmount] = useState(''); + + // Filter listings + const filteredListings = useMemo(() => { + return listings.filter(listing => { + const matchesSearch = + listing.title.toLowerCase().includes(searchQuery.toLowerCase()) || + listing.description.toLowerCase().includes(searchQuery.toLowerCase()) || + listing.business.name.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesCategory = selectedCategory === 'all' || listing.category === selectedCategory; + return matchesSearch && matchesCategory && listing.status === 'active'; + }); + }, [listings, searchQuery, selectedCategory]); + + // Refresh listings + const onRefresh = useCallback(async () => { + setRefreshing(true); + // TODO: Fetch from Supabase/blockchain + await new Promise(resolve => setTimeout(resolve, 1000)); + setRefreshing(false); + }, []); + + // Create new listing + const handleCreateListing = async () => { + if (!selectedAccount) { + Alert.alert('Şaşî / Error', 'Ji kerema xwe berê wallet ve girêbidin / Please connect wallet first'); + return; + } + + if (!newListing.title || !newListing.description || !newListing.price) { + Alert.alert('Şaşî / Error', 'Ji kerema xwe hemû qadan dagirin / Please fill all fields'); + return; + } + + // Create listing object + const listing: Listing = { + id: `listing_${Date.now()}`, + businessId: selectedAccount.address, + business: { + id: selectedAccount.address, + walletAddress: selectedAccount.address, + name: selectedAccount.meta?.name || 'My Business', + description: '', + region: newListing.location, + rating: 0, + reviewCount: 0, + verified: false, + createdAt: Date.now(), + }, + title: newListing.title, + description: newListing.description, + category: newListing.category, + price: parseFloat(newListing.price), + currency: 'HEZ', + unit: newListing.unit || 'piece', + minOrder: parseInt(newListing.minOrder) || 1, + images: [], + location: newListing.location, + isService: newListing.isService, + createdAt: Date.now(), + status: 'active', + }; + + // TODO: Save to Supabase/blockchain + setListings(prev => [listing, ...prev]); + setShowCreateModal(false); + setNewListing({ + title: '', + description: '', + category: 'services', + price: '', + unit: '', + minOrder: '1', + location: '', + isService: false, + }); + + Alert.alert( + 'Serketî / Success', + 'Îlana we hat çêkirin!\nYour listing has been created!', + [{ text: 'Temam / OK' }] + ); + }; + + // Send contact/offer + const handleSendContact = async () => { + if (!selectedAccount || !selectedListing) return; + + if (!contactMessage) { + Alert.alert('Şaşî / Error', 'Ji kerema xwe peyamekê binivîsin / Please write a message'); + return; + } + + // TODO: Send message via blockchain or Supabase + Alert.alert( + 'Serketî / Success', + `Peyama we ji ${selectedListing.business.name} re hat şandin!\nYour message has been sent!`, + [{ text: 'Temam / OK' }] + ); + + setShowContactModal(false); + setContactMessage(''); + setOfferAmount(''); + }; + + // Start escrow payment + const handleStartEscrow = async () => { + if (!selectedAccount || !selectedListing || !api) return; + + const amount = offerAmount || String(selectedListing.price * selectedListing.minOrder); + + Alert.alert( + 'Escrow Payment', + `Hûn ê ${amount} HEZ bişînin escrow.\nYou will send ${amount} HEZ to escrow.\n\nEv drav dê were parastin heta ku we mal/xizmet wergirt.\nFunds will be held until you receive goods/services.`, + [ + { text: 'Betal / Cancel', style: 'cancel' }, + { + text: 'Piştrast / Confirm', + onPress: async () => { + try { + const keyPair = await getKeyPair(selectedAccount.address); + if (!keyPair) throw new Error('KeyPair not found'); + + // TODO: Implement actual escrow transaction + Alert.alert( + 'Serketî / Success', + 'Escrow hate damezrandin!\nEscrow has been created!\n\nDrav di ewlehiyê de ye.\nFunds are secured.', + [{ text: 'Temam / OK' }] + ); + } catch (error) { + Alert.alert('Şaşî / Error', 'Escrow nehat damezrandin / Escrow failed'); + } + }, + }, + ] + ); + }; + + // Render category chip + const renderCategory = (category: Category | { id: 'all'; nameKu: string; nameEn: string; icon: string }) => ( + setSelectedCategory(category.id as CategoryType | 'all')} + > + {category.icon} + + {category.nameKu} + + + ); + + // Render listing card + const renderListing = ({ item }: { item: Listing }) => ( + { + setSelectedListing(item); + setShowDetailModal(true); + }} + activeOpacity={0.7} + > + + + + + {item.business.name.charAt(0)} + + + + + {item.business.name} + {item.business.verified && } + + {item.location} + + + + + {item.business.rating.toFixed(1)} + + + + {item.title} + + {item.description} + + + + + {item.price.toLocaleString()} + {item.currency} + /{item.unit} + + + + {CATEGORIES.find(c => c.id === item.category)?.icon} {item.isService ? 'Xizmet' : 'Hilber'} + + + + + + + Min. Order: {item.minOrder} {item.unit} + + + + ); + + return ( + + + + {/* Header */} + + navigation.goBack()} style={styles.backButton}> + ← Paş + + + B2B Bazirganî + Kurdish Business Marketplace + + { + if (!selectedAccount) { + Alert.alert('Şaşî / Error', 'Ji kerema xwe berê wallet ve girêbidin / Please connect wallet first'); + return; + } + setShowCreateModal(true); + }} + > + + Îlan + + + + {/* Search */} + + + 🔍 + + {searchQuery.length > 0 && ( + setSearchQuery('')}> + + + )} + + + + {/* Categories */} + + + {renderCategory({ id: 'all', nameKu: 'Hemû', nameEn: 'All', icon: '📋' })} + {CATEGORIES.map(renderCategory)} + + + + {/* Stats */} + + + {filteredListings.length} îlan / listings + + + + {/* Listings */} + item.id} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + refreshControl={ + + } + ListEmptyComponent={ + + 📭 + Îlan nehat dîtin + No listings found + + } + /> + + {/* Create Listing Modal */} + setShowCreateModal(false)} + > + + + setShowCreateModal(false)}> + + + Îlana Nû / New Listing + + + + + {/* Type Selection */} + + Cure / Type + + setNewListing(prev => ({ ...prev, isService: false }))} + > + 📦 + + Hilber / Product + + + setNewListing(prev => ({ ...prev, isService: true }))} + > + 💼 + + Xizmet / Service + + + + + + {/* Category */} + + Kategorî / Category + + + {CATEGORIES.map(cat => ( + setNewListing(prev => ({ ...prev, category: cat.id }))} + > + {cat.icon} + + {cat.nameKu} + + + ))} + + + + + {/* Title */} + + Sernav / Title * + setNewListing(prev => ({ ...prev, title: text }))} + placeholder="Navê hilber an xizmetê..." + placeholderTextColor="#999" + /> + + + {/* Description */} + + Danasîn / Description * + setNewListing(prev => ({ ...prev, description: text }))} + placeholder="Agahdariya berfireh li ser hilber an xizmetê..." + placeholderTextColor="#999" + multiline + numberOfLines={4} + /> + + + {/* Price & Unit */} + + + Biha / Price (HEZ) * + setNewListing(prev => ({ ...prev, price: text }))} + placeholder="0" + placeholderTextColor="#999" + keyboardType="decimal-pad" + /> + + + Yekîne / Unit + setNewListing(prev => ({ ...prev, unit: text }))} + placeholder="kg, piece..." + placeholderTextColor="#999" + /> + + + + {/* Min Order */} + + Kêmtirîn Spartin / Min Order + setNewListing(prev => ({ ...prev, minOrder: text }))} + placeholder="1" + placeholderTextColor="#999" + keyboardType="number-pad" + /> + + + {/* Location */} + + Cîh / Location + setNewListing(prev => ({ ...prev, location: text }))} + placeholder="Hewlêr, Amed, Diaspora..." + placeholderTextColor="#999" + /> + + + {/* Submit Button */} + + Îlan Çêke / Create Listing + + + + + + + + {/* Listing Detail Modal */} + setShowDetailModal(false)} + > + + {selectedListing && ( + <> + + setShowDetailModal(false)}> + + + Hûrgulî / Details + + + + + {/* Business Header */} + + + + {selectedListing.business.name.charAt(0)} + + + + + {selectedListing.business.name} + {selectedListing.business.verified && ( + + ✓ Verified + + )} + + 📍 {selectedListing.location} + + ⭐ {selectedListing.business.rating.toFixed(1)} + + ({selectedListing.business.reviewCount} reviews) + + + + + + {/* Listing Info */} + + {selectedListing.title} + + + {CATEGORIES.find(c => c.id === selectedListing.category)?.icon}{' '} + {CATEGORIES.find(c => c.id === selectedListing.category)?.nameKu} + + + + + + Danasîn / Description + {selectedListing.description} + + + {/* Price Card */} + + + Biha / Price + + + {selectedListing.price.toLocaleString()} + + {selectedListing.currency} + /{selectedListing.unit} + + + + Min. Order + + {selectedListing.minOrder} {selectedListing.unit} + + + + + {/* Action Buttons */} + + { + setShowDetailModal(false); + setShowContactModal(true); + }} + > + 💬 Pêwendî / Contact + + + 🔒 Escrow Payment + + + + + + + )} + + + + {/* Contact Modal */} + setShowContactModal(false)} + > + + + + Pêwendî bi {selectedListing?.business.name} + + + + Peyam / Message * + + + + + Pêşniyara Biha / Price Offer (HEZ) + + + + + setShowContactModal(false)} + > + Betal + + + Bişîne / Send + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F5F5F5', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: KurdistanColors.spi, + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: '#E0E0E0', + }, + backButton: { + padding: 4, + }, + backButtonText: { + fontSize: 16, + color: KurdistanColors.kesk, + fontWeight: '600', + }, + headerTitle: { + fontSize: 20, + fontWeight: 'bold', + color: KurdistanColors.reş, + }, + headerSubtitle: { + fontSize: 12, + color: '#666', + textAlign: 'center', + }, + addButton: { + backgroundColor: KurdistanColors.kesk, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 8, + }, + addButtonText: { + color: KurdistanColors.spi, + fontWeight: '600', + fontSize: 14, + }, + searchContainer: { + padding: 16, + backgroundColor: KurdistanColors.spi, + }, + searchBar: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#F8F9FA', + borderRadius: 12, + paddingHorizontal: 12, + height: 48, + borderWidth: 1, + borderColor: '#E0E0E0', + }, + searchIcon: { + fontSize: 18, + marginRight: 8, + }, + searchInput: { + flex: 1, + fontSize: 16, + color: '#333', + }, + clearIcon: { + fontSize: 16, + color: '#999', + padding: 4, + }, + categoriesContainer: { + backgroundColor: KurdistanColors.spi, + paddingBottom: 12, + }, + categoriesContent: { + paddingHorizontal: 16, + }, + categoryChip: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#F0F0F0', + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 20, + marginRight: 8, + }, + categoryChipActive: { + backgroundColor: KurdistanColors.kesk, + }, + categoryIcon: { + fontSize: 16, + marginRight: 6, + }, + categoryText: { + fontSize: 13, + fontWeight: '600', + color: '#555', + }, + categoryTextActive: { + color: KurdistanColors.spi, + }, + statsContainer: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: '#F5F5F5', + }, + statsText: { + fontSize: 12, + color: '#666', + }, + listContent: { + padding: 16, + paddingBottom: 100, + }, + listingCard: { + backgroundColor: KurdistanColors.spi, + borderRadius: 16, + padding: 16, + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + listingHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 12, + }, + businessInfo: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + businessAvatar: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: KurdistanColors.kesk, + justifyContent: 'center', + alignItems: 'center', + marginRight: 10, + }, + businessAvatarText: { + color: KurdistanColors.spi, + fontSize: 18, + fontWeight: 'bold', + }, + businessNameRow: { + flexDirection: 'row', + alignItems: 'center', + }, + businessName: { + fontSize: 14, + fontWeight: '600', + color: KurdistanColors.reş, + }, + verifiedBadge: { + color: KurdistanColors.kesk, + fontSize: 14, + marginLeft: 4, + fontWeight: 'bold', + }, + businessLocation: { + fontSize: 12, + color: '#666', + marginTop: 2, + }, + ratingContainer: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#FFF9E6', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + ratingStar: { + fontSize: 12, + }, + ratingText: { + fontSize: 13, + fontWeight: '600', + color: '#333', + marginLeft: 4, + }, + listingTitle: { + fontSize: 16, + fontWeight: '700', + color: KurdistanColors.reş, + marginBottom: 8, + }, + listingDescription: { + fontSize: 14, + color: '#666', + lineHeight: 20, + marginBottom: 12, + }, + listingFooter: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + priceContainer: { + flexDirection: 'row', + alignItems: 'baseline', + }, + priceValue: { + fontSize: 20, + fontWeight: 'bold', + color: KurdistanColors.kesk, + }, + priceCurrency: { + fontSize: 14, + fontWeight: '600', + color: KurdistanColors.kesk, + }, + priceUnit: { + fontSize: 12, + color: '#666', + }, + categoryBadge: { + backgroundColor: '#F0F0F0', + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + categoryBadgeText: { + fontSize: 12, + color: '#555', + }, + minOrderContainer: { + marginTop: 8, + paddingTop: 8, + borderTopWidth: 1, + borderTopColor: '#F0F0F0', + }, + minOrderText: { + fontSize: 12, + color: '#888', + }, + emptyContainer: { + alignItems: 'center', + paddingVertical: 60, + }, + emptyIcon: { + fontSize: 48, + marginBottom: 16, + }, + emptyText: { + fontSize: 18, + fontWeight: '600', + color: '#333', + }, + emptySubtext: { + fontSize: 14, + color: '#666', + marginTop: 4, + }, + // Modal Styles + modalContainer: { + flex: 1, + backgroundColor: '#F5F5F5', + }, + modalHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: KurdistanColors.spi, + paddingHorizontal: 16, + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: '#E0E0E0', + }, + modalClose: { + fontSize: 24, + color: '#666', + }, + modalTitle: { + fontSize: 18, + fontWeight: 'bold', + color: KurdistanColors.reş, + }, + modalContent: { + flex: 1, + padding: 16, + }, + formGroup: { + marginBottom: 20, + }, + formLabel: { + fontSize: 14, + fontWeight: '600', + color: KurdistanColors.reş, + marginBottom: 8, + }, + formInput: { + backgroundColor: KurdistanColors.spi, + borderRadius: 12, + padding: 14, + fontSize: 16, + borderWidth: 1, + borderColor: '#E0E0E0', + color: KurdistanColors.reş, + }, + formTextArea: { + height: 100, + textAlignVertical: 'top', + }, + formRow: { + flexDirection: 'row', + }, + 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: 28, + marginBottom: 8, + }, + typeText: { + fontSize: 14, + fontWeight: '600', + color: '#333', + }, + typeTextActive: { + color: KurdistanColors.kesk, + }, + categorySelector: { + flexDirection: 'row', + gap: 8, + }, + categorySelectorItem: { + alignItems: 'center', + backgroundColor: '#F8F9FA', + paddingHorizontal: 14, + paddingVertical: 10, + borderRadius: 12, + borderWidth: 2, + borderColor: 'transparent', + }, + categorySelectorItemActive: { + borderColor: KurdistanColors.kesk, + backgroundColor: `${KurdistanColors.kesk}10`, + }, + categorySelectorIcon: { + fontSize: 24, + marginBottom: 4, + }, + categorySelectorText: { + fontSize: 11, + fontWeight: '600', + color: '#666', + }, + categorySelectorTextActive: { + color: KurdistanColors.kesk, + }, + submitButton: { + backgroundColor: KurdistanColors.kesk, + borderRadius: 12, + padding: 16, + alignItems: 'center', + marginTop: 20, + }, + submitButtonText: { + fontSize: 16, + fontWeight: 'bold', + color: KurdistanColors.spi, + }, + // Detail Modal + detailBusinessCard: { + flexDirection: 'row', + backgroundColor: KurdistanColors.spi, + borderRadius: 16, + padding: 16, + marginBottom: 16, + }, + detailBusinessAvatar: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: KurdistanColors.kesk, + justifyContent: 'center', + alignItems: 'center', + marginRight: 16, + }, + detailBusinessAvatarText: { + color: KurdistanColors.spi, + fontSize: 24, + fontWeight: 'bold', + }, + detailBusinessInfo: { + flex: 1, + }, + detailBusinessName: { + fontSize: 18, + fontWeight: 'bold', + color: KurdistanColors.reş, + }, + verifiedBadgeLarge: { + backgroundColor: `${KurdistanColors.kesk}20`, + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 8, + marginLeft: 8, + }, + verifiedBadgeText: { + fontSize: 11, + color: KurdistanColors.kesk, + fontWeight: '600', + }, + detailBusinessLocation: { + fontSize: 14, + color: '#666', + marginTop: 4, + }, + detailRating: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 4, + }, + reviewCount: { + fontSize: 12, + color: '#888', + marginLeft: 4, + }, + detailSection: { + backgroundColor: KurdistanColors.spi, + borderRadius: 16, + padding: 16, + marginBottom: 16, + }, + detailTitle: { + fontSize: 20, + fontWeight: 'bold', + color: KurdistanColors.reş, + marginBottom: 12, + }, + detailCategoryBadge: { + backgroundColor: '#F0F0F0', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + alignSelf: 'flex-start', + }, + detailCategoryText: { + fontSize: 14, + color: '#555', + }, + detailSectionTitle: { + fontSize: 16, + fontWeight: '600', + color: KurdistanColors.reş, + marginBottom: 8, + }, + detailDescription: { + fontSize: 15, + color: '#444', + lineHeight: 22, + }, + priceCard: { + flexDirection: 'row', + justifyContent: 'space-between', + backgroundColor: `${KurdistanColors.kesk}10`, + borderRadius: 16, + padding: 20, + marginBottom: 16, + borderWidth: 1, + borderColor: `${KurdistanColors.kesk}30`, + }, + priceCardLabel: { + fontSize: 12, + color: '#666', + marginBottom: 4, + }, + priceCardValue: { + flexDirection: 'row', + alignItems: 'baseline', + }, + priceCardAmount: { + fontSize: 28, + fontWeight: 'bold', + color: KurdistanColors.kesk, + }, + priceCardCurrency: { + fontSize: 16, + fontWeight: '600', + color: KurdistanColors.kesk, + }, + priceCardUnit: { + fontSize: 14, + color: '#666', + }, + priceCardMinOrder: { + fontSize: 16, + fontWeight: '600', + color: KurdistanColors.reş, + }, + actionButtons: { + gap: 12, + }, + contactButton: { + backgroundColor: KurdistanColors.spi, + borderRadius: 12, + padding: 16, + alignItems: 'center', + borderWidth: 2, + borderColor: KurdistanColors.kesk, + }, + contactButtonText: { + fontSize: 16, + fontWeight: '600', + color: KurdistanColors.kesk, + }, + escrowButton: { + backgroundColor: KurdistanColors.kesk, + borderRadius: 12, + padding: 16, + alignItems: 'center', + }, + escrowButtonText: { + fontSize: 16, + fontWeight: 'bold', + color: KurdistanColors.spi, + }, + // Contact Modal + contactModalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + padding: 20, + }, + contactModalContent: { + backgroundColor: KurdistanColors.spi, + borderRadius: 16, + padding: 20, + }, + contactModalTitle: { + fontSize: 18, + fontWeight: 'bold', + color: KurdistanColors.reş, + marginBottom: 20, + textAlign: 'center', + }, + contactModalButtons: { + flexDirection: 'row', + gap: 12, + marginTop: 8, + }, + contactModalCancel: { + flex: 1, + backgroundColor: '#F0F0F0', + borderRadius: 10, + padding: 14, + alignItems: 'center', + }, + contactModalCancelText: { + fontSize: 16, + fontWeight: '600', + color: '#666', + }, + contactModalSend: { + flex: 1, + backgroundColor: KurdistanColors.kesk, + borderRadius: 10, + padding: 14, + alignItems: 'center', + }, + contactModalSendText: { + fontSize: 16, + fontWeight: 'bold', + color: KurdistanColors.spi, + }, +}); + +export default B2BScreen; diff --git a/mobile/src/screens/BeCitizenApplyScreen.tsx b/mobile/src/screens/BeCitizenApplyScreen.tsx index b75f6c7c..2927db80 100644 --- a/mobile/src/screens/BeCitizenApplyScreen.tsx +++ b/mobile/src/screens/BeCitizenApplyScreen.tsx @@ -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), diff --git a/mobile/src/screens/BeCitizenScreen.tsx b/mobile/src/screens/BeCitizenScreen.tsx index c6e2d9a0..38325433 100644 --- a/mobile/src/screens/BeCitizenScreen.tsx +++ b/mobile/src/screens/BeCitizenScreen.tsx @@ -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 ( - - - - - - - 🏛️ - - Be a Citizen - - Join the Pezkuwi decentralized nation - - - - - setCurrentStep('new')} - activeOpacity={0.8} - > - 📝 - New Citizen - - Apply for citizenship and join our community - - - - setCurrentStep('existing')} - activeOpacity={0.8} - > - 🔐 - Existing Citizen - - Access your citizenship account - - - - - - Citizenship Benefits - - - Voting rights in governance - - - - Access to exclusive services - - - - Referral rewards program - - - - Community recognition - - - - - - ); - } - - if (currentStep === 'new') { - return ( - - - - setCurrentStep('choice')} - > - ← Back - - - New Citizen Application - - Please provide your information to apply for citizenship - - - - Full Name * - - - - - Father's Name * - - - - - Mother's Name * - - - - - Tribe - - - - - Region - - - - - Email * - - - - - Profession - - - - - Referral Code - - - - - {isSubmitting ? ( - - ) : ( - Submit Application - )} - - - - - - ); - } - - // Existing Citizen Login return ( - - - setCurrentStep('choice')} - > - ← Back - - - Citizen Verification - - Verify your status using your connected wallet - - - - - Existing citizens are verified through their blockchain identity. Ensure your citizenship wallet is selected in the wallet tab. - - - - - {isSubmitting ? ( - - ) : ( - Verify Citizenship - )} - - + ); }; @@ -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', }, }); diff --git a/mobile/src/screens/DashboardScreen.tsx b/mobile/src/screens/DashboardScreen.tsx index ae745c92..fbbb532c 100644 --- a/mobile/src/screens/DashboardScreen.tsx +++ b/mobile/src/screens/DashboardScreen.tsx @@ -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 = () => { const [profileData, setProfileData] = useState(null); const [loading, setLoading] = useState(true); const [avatarModalVisible, setAvatarModalVisible] = useState(false); + const [notificationModalVisible, setNotificationModalVisible] = useState(false); // Blockchain state const [tikis, setTikis] = useState([]); @@ -175,6 +177,38 @@ const DashboardScreen: React.FC = () => { ); }; + 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 = () => { - showComingSoon('Notifications')}> + setNotificationModalVisible(true)}> 🔔 navigation.navigate('Settings')}> @@ -411,7 +445,7 @@ const DashboardScreen: React.FC = () => { {kycStatus === 'NotStarted' && ( navigation.navigate('BeCitizenChoice')} + onPress={() => navigation.navigate('BeCitizen')} > Apply @@ -432,12 +466,12 @@ const DashboardScreen: React.FC = () => { {/* 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)} @@ -447,14 +481,14 @@ const DashboardScreen: React.FC = () => { GOVERNANCE 🏛️ - {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)} @@ -465,8 +499,8 @@ const DashboardScreen: React.FC = () => { {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 = () => { EDUCATION 📚 - {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)} @@ -502,6 +532,12 @@ const DashboardScreen: React.FC = () => { currentAvatar={profileData?.avatar_url} onAvatarSelected={handleAvatarSelected} /> + + {/* Notification Center Modal */} + setNotificationModalVisible(false)} + /> ); }; diff --git a/mobile/src/screens/IdentityScreen.tsx b/mobile/src/screens/IdentityScreen.tsx new file mode 100644 index 00000000..cea74d84 --- /dev/null +++ b/mobile/src/screens/IdentityScreen.tsx @@ -0,0 +1,1112 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + SafeAreaView, + ScrollView, + StatusBar, + ActivityIndicator, + RefreshControl, +} 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 { + fetchUserTikiNFTs, + getCitizenNFTDetails, + getTikiDisplayName, + getTikiEmoji, + ROLE_CATEGORIES, + type TikiNFTDetails, +} from '../../shared/lib/tiki'; + +/** + * Identity Screen + * + * Shows user's digital identity: + * - Citizens: Welati NFT card + other role NFTs + * - Visitors: Digital Kurdistan State Visa Card + any NFTs they have + */ +const IdentityScreen: React.FC = () => { + const navigation = useNavigation(); + const { api, isApiReady, selectedAccount } = usePezkuwi(); + + // Choice state + const [userChoice, setUserChoice] = useState<'citizen' | 'visitor' | null>(null); + + // Loading & data state + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [citizenNFT, setCitizenNFT] = useState(null); + const [otherNFTs, setOtherNFTs] = useState([]); + const [citizenCheckDone, setCitizenCheckDone] = useState(false); + const [isActuallyCitizen, setIsActuallyCitizen] = useState(false); + + const fetchNFTData = useCallback(async () => { + if (!api || !isApiReady || !selectedAccount) { + setLoading(false); + return; + } + + try { + setLoading(true); + + // Fetch citizen NFT + const citizenDetails = await getCitizenNFTDetails(api, selectedAccount.address); + setCitizenNFT(citizenDetails); + setIsActuallyCitizen(!!citizenDetails); + + // Fetch all tiki NFTs + const allTikis = await fetchUserTikiNFTs(api, selectedAccount.address); + + // Filter out Welati from other NFTs (it's shown separately) + const others = allTikis.filter(nft => nft.tikiRole !== 'Welati'); + setOtherNFTs(others); + + setCitizenCheckDone(true); + + } catch (error) { + if (__DEV__) console.error('[Identity] Error fetching NFTs:', error); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [api, isApiReady, selectedAccount]); + + // Fetch data when user makes a choice + useEffect(() => { + if (userChoice && selectedAccount) { + fetchNFTData(); + } + }, [userChoice, selectedAccount, fetchNFTData]); + + const onRefresh = () => { + setRefreshing(true); + fetchNFTData(); + }; + + // Get category for a tiki + const getTikiCategory = (tiki: string): string => { + for (const [category, roles] of Object.entries(ROLE_CATEGORIES)) { + if (roles.includes(tiki)) { + return category; + } + } + return 'Other'; + }; + + // Format address + const formatAddress = (address: string) => { + return `${address.slice(0, 8)}...${address.slice(-6)}`; + }; + + // Generate visa number from address + const generateVisaNumber = (address: string) => { + const hash = address.slice(2, 10).toUpperCase(); + return `VIS-${hash}-KRD`; + }; + + // No wallet connected + if (!selectedAccount) { + return ( + + + + + 🔗 + Wallet Required + + Please connect your wallet to view your digital identity. + + navigation.navigate('Wallet' as never)} + > + Connect Wallet + + navigation.goBack()} + > + ← Go Back + + + + + ); + } + + // Choice screen - Ask if citizen or visitor + if (!userChoice) { + return ( + + + + navigation.goBack()} style={styles.choiceBackButton}> + ← Back + + + + 🆔 + Nasnameya Dîjîtal + Digital Identity + + Hûn welatî ne? + Are you a citizen? + + + setUserChoice('citizen')} + activeOpacity={0.8} + > + + 👤 + + Ez welatî me + I am a citizen + + + setUserChoice('visitor')} + activeOpacity={0.8} + > + + 🌍 + + Ez ne welatî me + Not a citizen + + + + + + ); + } + + // Main content after choice + return ( + + + + {/* Header */} + + setUserChoice(null)} style={styles.headerBackButton}> + + + + + 🆔 + Nasnameya Dîjîtal + Digital Identity + + + {/* Kurdistan Sun decoration */} + + + {[...Array(21)].map((_, i) => ( + + ))} + + + + + } + > + {loading ? ( + + + Loading your identity... + + ) : ( + <> + {/* Wallet Address Card */} + + Connected Wallet + {formatAddress(selectedAccount.address)} + + + {/* CITIZEN PATH */} + {userChoice === 'citizen' && ( + <> + {citizenCheckDone && !isActuallyCitizen ? ( + // User selected citizen but doesn't have Welati + + ⚠️ + Citizenship Not Found + + We couldn't find a Welati (citizen) NFT for this wallet. + Please apply for citizenship to get your digital identity. + + navigation.navigate('BeCitizen' as never)} + > + Apply for Citizenship + + + ) : citizenNFT ? ( + // Citizen NFT Card + + + + + NFT + + + + 👤 + + + WELATI + Citizen of Digital Kurdistan + + + + Collection ID + #{citizenNFT.collectionId} + + + Item ID + #{citizenNFT.itemId} + + + Score Bonus + +{citizenNFT.tikiScore} + + + + + ✓ Verified Citizen + + + + + ) : null} + + )} + + {/* VISITOR PATH - Digital Kurdistan State Visa Card */} + {userChoice === 'visitor' && ( + + + + {/* Top Banner */} + + KOMARA DÎJÎTAL A KURDISTANÊ + DIGITAL REPUBLIC OF KURDISTAN + + + {/* Visa Type */} + + STATE VISA + + + {/* Kurdistan Flag Colors Bar */} + + + + + + + {/* Photo placeholder with globe */} + + 🌍 + + + {/* Visa Details */} + + + VISA NUMBER + + {generateVisaNumber(selectedAccount.address)} + + + + STATUS + VISITOR + + + WALLET + + {formatAddress(selectedAccount.address)} + + + + ACCESS LEVEL + LIMITED + + + + {/* Sun emblem */} + + ☀️ + + + {/* Bottom info */} + + + This visa grants limited access to Digital Kurdistan services. + + + Apply for citizenship for full access. + + + + + + {/* Become Citizen CTA */} + navigation.navigate('BeCitizen' as never)} + > + Upgrade to Citizenship + + + )} + + {/* Other Tikis Section */} + + Tikiyên Din / Other NFTs + {otherNFTs.length} NFT + + + {otherNFTs.length === 0 ? ( + + + {userChoice === 'citizen' + ? 'No other role NFTs yet. Participate in governance, get elected, or earn roles to receive more tikis.' + : 'No NFTs found. As a visitor, you can still explore the ecosystem. Consider applying for citizenship to unlock more features.'} + + + ) : ( + + {otherNFTs.map((nft, index) => ( + + + {nft.tikiEmoji} + + + {nft.tikiRole} + {nft.tikiDisplayName} + + + {getTikiCategory(nft.tikiRole)} + + + +{nft.tikiScore} pts + + + ))} + + )} + + {/* Total Score - only show if has NFTs */} + {(citizenNFT || otherNFTs.length > 0) && ( + + Total Tiki Score + + {(citizenNFT?.tikiScore || 0) + otherNFTs.reduce((sum, nft) => sum + nft.tikiScore, 0)} + + + {(citizenNFT ? 1 : 0) + otherNFTs.length} NFT{(citizenNFT ? 1 : 0) + otherNFTs.length !== 1 ? 's' : ''} total + + + )} + + {/* Info Section */} + + About Digital Identity + + {userChoice === 'citizen' ? ( + <> + Your Welati NFT is your digital citizenship in the Digital Republic of Kurdistan. + It grants you voting rights, access to governance, and participation in the ecosystem. + {'\n\n'} + Additional role NFTs (Tikis) represent your achievements and positions within the republic. + + ) : ( + <> + As a visitor, you have limited access to Digital Kurdistan services. + Your State Visa allows basic exploration of the ecosystem. + {'\n\n'} + To unlock full benefits including voting rights, governance participation, + and exclusive services, consider applying for citizenship. + + )} + + + + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F5F5F5', + }, + + // No Wallet + noWalletContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + noWalletContent: { + 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, + }, + noWalletIcon: { + fontSize: 64, + marginBottom: 16, + }, + noWalletTitle: { + fontSize: 24, + fontWeight: 'bold', + color: KurdistanColors.reş, + marginBottom: 12, + }, + noWalletText: { + fontSize: 14, + color: '#666', + textAlign: 'center', + marginBottom: 24, + lineHeight: 22, + }, + connectButton: { + backgroundColor: KurdistanColors.kesk, + paddingHorizontal: 32, + paddingVertical: 14, + borderRadius: 12, + marginBottom: 12, + }, + connectButtonText: { + color: KurdistanColors.spi, + fontSize: 16, + fontWeight: 'bold', + }, + backButton: { + padding: 12, + }, + backButtonText: { + color: KurdistanColors.kesk, + fontSize: 16, + }, + + // Choice Screen + choiceContainer: { + flex: 1, + }, + choiceBackButton: { + position: 'absolute', + top: 50, + left: 20, + zIndex: 10, + padding: 8, + }, + choiceBackText: { + fontSize: 16, + color: KurdistanColors.spi, + fontWeight: '600', + }, + choiceContent: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + choiceIcon: { + fontSize: 64, + marginBottom: 16, + }, + choiceTitle: { + fontSize: 28, + fontWeight: 'bold', + color: KurdistanColors.spi, + marginBottom: 4, + }, + choiceSubtitle: { + fontSize: 16, + color: 'rgba(255,255,255,0.8)', + marginBottom: 40, + }, + choiceQuestion: { + fontSize: 22, + fontWeight: '600', + color: KurdistanColors.spi, + marginBottom: 4, + }, + choiceQuestionEn: { + fontSize: 16, + color: 'rgba(255,255,255,0.8)', + marginBottom: 32, + }, + choiceCards: { + flexDirection: 'row', + gap: 16, + }, + choiceCard: { + backgroundColor: 'rgba(255,255,255,0.95)', + borderRadius: 20, + padding: 24, + alignItems: 'center', + width: 150, + boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.2)', + elevation: 6, + }, + choiceCardIcon: { + width: 60, + height: 60, + borderRadius: 30, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 12, + }, + choiceCardEmoji: { + fontSize: 32, + }, + choiceCardTitle: { + fontSize: 14, + fontWeight: 'bold', + color: KurdistanColors.reş, + textAlign: 'center', + marginBottom: 4, + }, + choiceCardSubtitle: { + fontSize: 12, + color: '#666', + textAlign: 'center', + }, + + // Header + header: { + paddingTop: 20, + paddingBottom: 30, + paddingHorizontal: 20, + position: 'relative', + overflow: 'hidden', + }, + headerBackButton: { + position: 'absolute', + top: 20, + left: 16, + zIndex: 10, + padding: 8, + }, + headerBackText: { + fontSize: 24, + color: KurdistanColors.spi, + }, + headerContent: { + alignItems: 'center', + marginTop: 10, + }, + headerIcon: { + fontSize: 48, + marginBottom: 8, + }, + headerTitle: { + fontSize: 24, + fontWeight: 'bold', + color: KurdistanColors.spi, + marginBottom: 4, + }, + headerSubtitle: { + fontSize: 14, + color: 'rgba(255,255,255,0.8)', + }, + + // Sun decoration + sunDecoration: { + position: 'absolute', + right: -30, + top: -30, + width: 120, + height: 120, + opacity: 0.2, + }, + sunCenter: { + position: 'absolute', + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: KurdistanColors.zer, + left: 40, + top: 40, + }, + sunRay: { + position: 'absolute', + width: 3, + height: 60, + backgroundColor: KurdistanColors.zer, + left: 58, + top: 0, + transformOrigin: 'center bottom', + }, + + // Content + content: { + flex: 1, + }, + contentContainer: { + padding: 16, + paddingBottom: 40, + }, + + // Loading + loadingContainer: { + padding: 60, + alignItems: 'center', + }, + loadingText: { + marginTop: 16, + color: '#666', + }, + + // Wallet Card + walletCard: { + backgroundColor: KurdistanColors.spi, + borderRadius: 12, + padding: 16, + marginBottom: 20, + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.05)', + elevation: 2, + }, + walletLabel: { + fontSize: 12, + color: '#666', + marginBottom: 4, + }, + walletAddress: { + fontSize: 16, + fontFamily: 'monospace', + color: KurdistanColors.reş, + fontWeight: '600', + }, + + // Not Citizen Alert + notCitizenAlert: { + backgroundColor: '#FFF3E0', + borderRadius: 16, + padding: 24, + alignItems: 'center', + marginBottom: 24, + borderWidth: 1, + borderColor: '#FFB74D', + }, + notCitizenIcon: { + fontSize: 48, + marginBottom: 12, + }, + notCitizenTitle: { + fontSize: 18, + fontWeight: 'bold', + color: '#E65100', + marginBottom: 8, + }, + notCitizenText: { + fontSize: 14, + color: '#666', + textAlign: 'center', + marginBottom: 20, + lineHeight: 22, + }, + applyButton: { + backgroundColor: KurdistanColors.kesk, + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 10, + }, + applyButtonText: { + color: KurdistanColors.spi, + fontWeight: 'bold', + fontSize: 14, + }, + + // Citizen Card + citizenCard: { + borderRadius: 20, + overflow: 'hidden', + marginBottom: 24, + boxShadow: '0px 8px 24px rgba(0, 128, 0, 0.2)', + elevation: 8, + }, + citizenCardGradient: { + padding: 3, + }, + citizenCardInner: { + backgroundColor: KurdistanColors.spi, + borderRadius: 17, + padding: 24, + alignItems: 'center', + }, + nftBadge: { + position: 'absolute', + top: 16, + right: 16, + backgroundColor: KurdistanColors.kesk, + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 8, + }, + nftBadgeText: { + color: KurdistanColors.spi, + fontSize: 10, + fontWeight: 'bold', + }, + citizenIconContainer: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: `${KurdistanColors.kesk}15`, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 16, + borderWidth: 3, + borderColor: KurdistanColors.kesk, + }, + citizenIcon: { + fontSize: 40, + }, + citizenTitle: { + fontSize: 28, + fontWeight: 'bold', + color: KurdistanColors.kesk, + letterSpacing: 4, + marginBottom: 4, + }, + citizenSubtitle: { + fontSize: 14, + color: '#666', + marginBottom: 20, + }, + citizenDetails: { + width: '100%', + backgroundColor: '#F8F9FA', + borderRadius: 12, + padding: 16, + marginBottom: 16, + }, + detailRow: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 8, + borderBottomWidth: 1, + borderBottomColor: '#E0E0E0', + }, + detailLabel: { + fontSize: 14, + color: '#666', + }, + detailValue: { + fontSize: 14, + fontWeight: '600', + color: KurdistanColors.reş, + }, + verifiedBadge: { + backgroundColor: '#E8F5E9', + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 20, + }, + verifiedText: { + color: KurdistanColors.kesk, + fontWeight: 'bold', + fontSize: 14, + }, + + // Visa Card + visaCard: { + marginBottom: 24, + }, + visaCardGradient: { + borderRadius: 20, + padding: 2, + }, + visaCardInner: { + backgroundColor: '#1a237e', + borderRadius: 18, + padding: 20, + alignItems: 'center', + }, + visaTopBanner: { + alignItems: 'center', + marginBottom: 16, + }, + visaCountryName: { + fontSize: 12, + fontWeight: 'bold', + color: KurdistanColors.zer, + letterSpacing: 1, + }, + visaCountryNameEn: { + fontSize: 10, + color: 'rgba(255,255,255,0.7)', + letterSpacing: 0.5, + }, + visaTypeBadge: { + backgroundColor: KurdistanColors.sor, + paddingHorizontal: 20, + paddingVertical: 6, + borderRadius: 4, + marginBottom: 16, + }, + visaTypeText: { + color: KurdistanColors.spi, + fontWeight: 'bold', + fontSize: 14, + letterSpacing: 2, + }, + visaFlagBar: { + flexDirection: 'row', + width: '100%', + height: 6, + borderRadius: 3, + overflow: 'hidden', + marginBottom: 20, + }, + visaFlagStripe: { + flex: 1, + }, + visaPhotoContainer: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: 'rgba(255,255,255,0.1)', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + borderWidth: 2, + borderColor: 'rgba(255,255,255,0.3)', + }, + visaPhotoEmoji: { + fontSize: 40, + }, + visaDetails: { + width: '100%', + backgroundColor: 'rgba(255,255,255,0.1)', + borderRadius: 12, + padding: 16, + marginBottom: 16, + }, + visaDetailRow: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 6, + borderBottomWidth: 1, + borderBottomColor: 'rgba(255,255,255,0.1)', + }, + visaDetailLabel: { + fontSize: 10, + color: 'rgba(255,255,255,0.6)', + fontWeight: '600', + }, + visaDetailValue: { + fontSize: 12, + color: KurdistanColors.spi, + fontWeight: 'bold', + fontFamily: 'monospace', + }, + visaSunEmblem: { + marginBottom: 16, + }, + visaSunText: { + fontSize: 32, + }, + visaBottom: { + alignItems: 'center', + }, + visaBottomText: { + fontSize: 10, + color: 'rgba(255,255,255,0.5)', + textAlign: 'center', + }, + upgradeButton: { + backgroundColor: KurdistanColors.kesk, + padding: 16, + borderRadius: 12, + alignItems: 'center', + marginTop: 12, + }, + upgradeButtonText: { + color: KurdistanColors.spi, + fontWeight: 'bold', + fontSize: 16, + }, + + // Section Header + sectionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + marginTop: 8, + }, + sectionTitle: { + fontSize: 18, + fontWeight: 'bold', + color: KurdistanColors.reş, + }, + sectionCount: { + fontSize: 14, + color: '#666', + }, + + // No Other NFTs + noOtherNFTs: { + backgroundColor: KurdistanColors.spi, + borderRadius: 12, + padding: 24, + marginBottom: 24, + }, + noOtherText: { + fontSize: 14, + color: '#666', + textAlign: 'center', + lineHeight: 22, + }, + + // NFT Grid + nftGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 12, + marginBottom: 24, + }, + nftCard: { + width: '48%', + backgroundColor: KurdistanColors.spi, + borderRadius: 16, + overflow: 'hidden', + boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.08)', + elevation: 3, + }, + nftCardHeader: { + padding: 16, + alignItems: 'center', + }, + nftCardEmoji: { + fontSize: 36, + }, + nftCardBody: { + padding: 12, + alignItems: 'center', + }, + nftCardRole: { + fontSize: 12, + fontWeight: 'bold', + color: KurdistanColors.kesk, + marginBottom: 2, + }, + nftCardName: { + fontSize: 13, + color: KurdistanColors.reş, + textAlign: 'center', + marginBottom: 8, + }, + nftCardCategory: { + backgroundColor: '#F0F0F0', + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4, + marginBottom: 8, + }, + nftCardCategoryText: { + fontSize: 10, + color: '#666', + }, + nftCardScore: { + fontSize: 14, + fontWeight: 'bold', + color: KurdistanColors.zer, + }, + + // Total Score + totalScoreCard: { + backgroundColor: KurdistanColors.kesk, + borderRadius: 16, + padding: 24, + alignItems: 'center', + marginBottom: 24, + }, + totalScoreLabel: { + fontSize: 14, + color: 'rgba(255,255,255,0.8)', + marginBottom: 8, + }, + totalScoreValue: { + fontSize: 48, + fontWeight: 'bold', + color: KurdistanColors.spi, + }, + totalScoreSubtext: { + fontSize: 14, + color: 'rgba(255,255,255,0.8)', + marginTop: 4, + }, + + // Info Section + infoSection: { + backgroundColor: KurdistanColors.spi, + borderRadius: 16, + padding: 20, + }, + infoTitle: { + fontSize: 16, + fontWeight: 'bold', + color: KurdistanColors.reş, + marginBottom: 12, + }, + infoText: { + fontSize: 14, + color: '#666', + lineHeight: 22, + }, +}); + +export default IdentityScreen; diff --git a/mobile/src/screens/KurdMediaScreen.tsx b/mobile/src/screens/KurdMediaScreen.tsx new file mode 100644 index 00000000..1b8ec031 --- /dev/null +++ b/mobile/src/screens/KurdMediaScreen.tsx @@ -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) => ( + handleMediaPress(channel)} + activeOpacity={0.7} + > + + {channel.icon} + + + {channel.nameKu} + + {channel.descriptionKu} + + + + Soon + + + ); + + const renderSocialPlatform = (platform: SocialPlatform) => ( + handleSocialPress(platform)} + activeOpacity={0.7} + > + + {platform.icon} + + {platform.name} + + ); + + return ( + + + + {/* Header */} + + navigation.goBack()} + > + + + + KurdMedia + Medyaya Kurdî & Piştgirî + + + + + {/* Kurdish Media Section */} + + + + 📡 + + + Medyaya Kurdî + Kurdish Media + + + + + + Weşanên fermî yên Dewleta Dijîtal a Kurdistanê. TV, radyo, nûçe û bêtir. + + + Official broadcasts of Digital Kurdistan State. TV, radio, news and more. + + + + {MEDIA_CHANNELS.map(renderMediaChannel)} + + + + + {/* Support PezkuwiChain Section */} + + + + 🤝 + + + Piştgirî PezkuwiChain + Support PezkuwiChain + + + + + + Bi me re têkildar bin li ser platformên civakî. Pirsan bipirsin, nûçeyan bişopînin û bên beşdarî civata me. + + + Connect with us on social platforms. Ask questions, follow news and join our community. + + + + {SOCIAL_PLATFORMS.map(renderSocialPlatform)} + + + {/* Community Stats */} + + + 40M+ + Kurd li cîhanê + + + + 5B + PEZ Total + + + + + Hêvî / Hope + + + + + + {/* Info Banner */} + + 💡 + + + PezkuwiChain - Blockchain'a yekem a netewî ya Kurdan + + + PezkuwiChain - The first national blockchain of the Kurds + + + + + {/* Bottom Spacing */} + + + + ); +}; + +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; diff --git a/mobile/src/screens/LaunchpadScreen.tsx b/mobile/src/screens/LaunchpadScreen.tsx new file mode 100644 index 00000000..8bac46a2 --- /dev/null +++ b/mobile/src/screens/LaunchpadScreen.tsx @@ -0,0 +1,1183 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + SafeAreaView, + ScrollView, + TouchableOpacity, + TextInput, + Alert, + ActivityIndicator, + RefreshControl, + StatusBar, + Linking, + FlatList, + Modal, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { KurdistanColors } from '../theme/colors'; +import { usePezkuwi } from '../contexts/PezkuwiContext'; + +// Types from pezpallet-presale +type PresaleStatus = 'Pending' | 'Active' | 'Paused' | 'Successful' | 'Failed' | 'Cancelled' | 'Finalized'; + +interface ContributionLimits { + minContribution: string; + maxContribution: string; + softCap: string; + hardCap: string; +} + +interface VestingSchedule { + immediateReleasePercent: number; + vestingDurationBlocks: number; + cliffBlocks: number; +} + +interface PresaleConfig { + id: number; + owner: string; + paymentAsset: number; + rewardAsset: number; + tokensForSale: string; + startBlock: number; + duration: number; + status: PresaleStatus; + accessControl: 'Public' | 'Whitelist'; + limits: ContributionLimits; + vesting: VestingSchedule | null; + gracePeriodBlocks: number; + refundFeePercent: number; + graceRefundFeePercent: number; + // Computed fields + totalRaised: string; + contributorCount: number; + endBlock: number; + progress: number; + timeRemaining: { days: number; hours: number; minutes: number }; +} + +interface ContributionInfo { + amount: string; + contributedAt: number; + refunded: boolean; +} + +const BLOCK_TIME_SECONDS = 6; +const PLATFORM_FEE_PERCENT = 2; + +const LaunchpadScreen: React.FC = () => { + const navigation = useNavigation(); + const { api, selectedAccount, isApiReady, getKeyPair } = usePezkuwi(); + + const [presales, setPresales] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [contributing, setContributing] = useState(false); + const [selectedPresale, setSelectedPresale] = useState(null); + const [contributionAmount, setContributionAmount] = useState(''); + const [userContributions, setUserContributions] = useState>({}); + const [assetBalances, setAssetBalances] = useState>({}); + const [showDetailModal, setShowDetailModal] = useState(false); + const [currentBlock, setCurrentBlock] = useState(0); + + // Fetch all presales from chain + const fetchPresales = useCallback(async () => { + if (!api || !isApiReady) { + setLoading(false); + return; + } + + try { + // Get current block + const header = await api.rpc.chain.getHeader(); + const blockNum = header.number.toNumber(); + setCurrentBlock(blockNum); + + // Get next presale ID to know how many presales exist + const nextId = await api.query.presale?.nextPresaleId?.(); + const maxPresaleId = nextId ? parseInt(nextId.toString()) : 0; + + if (maxPresaleId === 0) { + setPresales([]); + setLoading(false); + setRefreshing(false); + return; + } + + const presaleList: PresaleConfig[] = []; + + // Fetch each presale + for (let id = 0; id < maxPresaleId; id++) { + const presaleData = await api.query.presale?.presales?.(id); + if (!presaleData || presaleData.isNone) continue; + + const config = presaleData.toJSON() as any; + if (!config) continue; + + // Get total raised and contributors + const totalRaised = await api.query.presale?.totalRaised?.(id); + const contributors = await api.query.presale?.contributors?.(id); + + const startBlock = config.startBlock || 0; + const duration = config.duration || 0; + const endBlock = startBlock + duration; + const totalRaisedStr = totalRaised?.toString() || '0'; + const hardCap = config.limits?.hardCap || '0'; + + // Calculate progress + const progress = hardCap !== '0' + ? Math.min(100, (parseFloat(totalRaisedStr) / parseFloat(hardCap)) * 100) + : 0; + + // Calculate time remaining + const blocksRemaining = Math.max(0, endBlock - blockNum); + const secondsRemaining = blocksRemaining * BLOCK_TIME_SECONDS; + const timeRemaining = { + days: Math.floor(secondsRemaining / 86400), + hours: Math.floor((secondsRemaining % 86400) / 3600), + minutes: Math.floor((secondsRemaining % 3600) / 60), + }; + + presaleList.push({ + id, + owner: config.owner || '', + paymentAsset: config.paymentAsset || 0, + rewardAsset: config.rewardAsset || 0, + tokensForSale: config.tokensForSale?.toString() || '0', + startBlock, + duration, + status: config.status || 'Pending', + accessControl: config.accessControl || 'Public', + limits: { + minContribution: config.limits?.minContribution?.toString() || '0', + maxContribution: config.limits?.maxContribution?.toString() || '0', + softCap: config.limits?.softCap?.toString() || '0', + hardCap: hardCap.toString(), + }, + vesting: config.vesting || null, + gracePeriodBlocks: config.gracePeriodBlocks || 0, + refundFeePercent: config.refundFeePercent || 0, + graceRefundFeePercent: config.graceRefundFeePercent || 0, + totalRaised: totalRaisedStr, + contributorCount: (contributors?.toHuman() as string[])?.length || 0, + endBlock, + progress, + timeRemaining, + }); + } + + // Fetch user contributions if wallet connected + if (selectedAccount?.address) { + const userContribs: Record = {}; + const balances: Record = {}; + + for (const presale of presaleList) { + // Get user's contribution for this presale + const contribution = await api.query.presale?.contributions?.(presale.id, selectedAccount.address); + if (contribution && !contribution.isNone) { + const contribData = contribution.toJSON() as any; + userContribs[presale.id] = { + amount: contribData?.amount?.toString() || '0', + contributedAt: contribData?.contributedAt || 0, + refunded: contribData?.refunded || false, + }; + } + + // Get payment asset balance + if (!balances[presale.paymentAsset]) { + const assetAccount = await api.query.assets?.account?.(presale.paymentAsset, selectedAccount.address); + balances[presale.paymentAsset] = assetAccount?.balance?.toString() || '0'; + } + } + + setUserContributions(userContribs); + setAssetBalances(balances); + } + + // Sort by status: Active first, then by ID desc + presaleList.sort((a, b) => { + const statusOrder: Record = { + Active: 0, Pending: 1, Paused: 2, Successful: 3, Failed: 4, Finalized: 5, Cancelled: 6 + }; + if (statusOrder[a.status] !== statusOrder[b.status]) { + return statusOrder[a.status] - statusOrder[b.status]; + } + return b.id - a.id; + }); + + setPresales(presaleList); + } catch (error) { + if (__DEV__) console.error('Error fetching presales:', error); + // Demo data for offline/testing + setPresales([ + { + id: 0, + owner: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + paymentAsset: 1000, // wUSDT + rewardAsset: 1, // PEZ + tokensForSale: '10000000000000000000', // 10M PEZ + startBlock: 100000, + duration: 648000, + status: 'Active', + accessControl: 'Public', + limits: { + minContribution: '1000000', // 1 wUSDT + maxContribution: '100000000000', // 100K wUSDT + softCap: '50000000000', // 50K wUSDT + hardCap: '500000000000', // 500K wUSDT + }, + vesting: null, + gracePeriodBlocks: 14400, + refundFeePercent: 5, + graceRefundFeePercent: 1, + totalRaised: '125000000000', + contributorCount: 847, + endBlock: 748000, + progress: 25, + timeRemaining: { days: 32, hours: 14, minutes: 22 }, + }, + ]); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [api, isApiReady, selectedAccount?.address]); + + useEffect(() => { + fetchPresales(); + }, [fetchPresales]); + + const onRefresh = useCallback(() => { + setRefreshing(true); + fetchPresales(); + }, [fetchPresales]); + + // Format asset amount with decimals + const formatAmount = (amount: string, decimals: number = 6): string => { + const num = parseFloat(amount) / Math.pow(10, decimals); + if (num >= 1000000) return (num / 1000000).toFixed(2) + 'M'; + if (num >= 1000) return (num / 1000).toFixed(2) + 'K'; + return num.toLocaleString('en-US', { maximumFractionDigits: 2 }); + }; + + // Get status color + const getStatusColor = (status: PresaleStatus): string => { + switch (status) { + case 'Active': return KurdistanColors.kesk; + case 'Pending': return '#FFA000'; + case 'Paused': return '#FF9800'; + case 'Successful': return '#4CAF50'; + case 'Finalized': return '#2196F3'; + case 'Failed': return '#F44336'; + case 'Cancelled': return '#9E9E9E'; + default: return '#999'; + } + }; + + // Handle contribute + const handleContribute = async () => { + if (!api || !selectedAccount || !selectedPresale) { + Alert.alert('Wallet Pêwîst e', 'Please connect your wallet first.'); + return; + } + + const amount = parseFloat(contributionAmount); + if (!amount || amount <= 0) { + Alert.alert('Mîqdar Çewt', 'Please enter a valid amount.'); + return; + } + + // Assume payment asset has 6 decimals (wUSDT) + const amountInUnits = BigInt(Math.floor(amount * 1000000)); + const balance = BigInt(assetBalances[selectedPresale.paymentAsset] || '0'); + + if (amountInUnits > balance) { + Alert.alert('Têrê Nake', `Insufficient balance. You have ${formatAmount(balance.toString())} wUSDT.`); + return; + } + + const minContrib = BigInt(selectedPresale.limits.minContribution); + const maxContrib = BigInt(selectedPresale.limits.maxContribution); + const existingContrib = BigInt(userContributions[selectedPresale.id]?.amount || '0'); + const newTotal = existingContrib + amountInUnits; + + if (newTotal < minContrib) { + Alert.alert('Minimum Nake', `Minimum contribution: ${formatAmount(minContrib.toString())} wUSDT`); + return; + } + + if (newTotal > maxContrib) { + Alert.alert('Maximum Derket', `Maximum contribution: ${formatAmount(maxContrib.toString())} wUSDT`); + return; + } + + const keyPair = await getKeyPair(selectedAccount.address); + if (!keyPair) { + Alert.alert('Çewtî', 'Could not get keyPair.'); + return; + } + + // Platform fee info + const fee = (amount * PLATFORM_FEE_PERCENT) / 100; + const netAmount = amount - fee; + + Alert.alert( + 'Piştrastî Bike / Confirm', + `Mîqdar: ${amount} wUSDT\nFee (${PLATFORM_FEE_PERCENT}%): ${fee.toFixed(2)} wUSDT\nNet: ${netAmount.toFixed(2)} wUSDT\n\nPresale #${selectedPresale.id}`, + [ + { text: 'Betal', style: 'cancel' }, + { + text: 'Piştrastî', + onPress: async () => { + setContributing(true); + try { + const kp = await getKeyPair(selectedAccount.address); + if (!kp) throw new Error('KeyPair not found'); + + await new Promise((resolve, reject) => { + api.tx.presale + .contribute(selectedPresale.id, amountInUnits.toString()) + .signAndSend(kp, { nonce: -1 }, ({ status, dispatchError }) => { + if (status.isInBlock || status.isFinalized) { + 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())); + } + } else { + resolve(); + } + } + }) + .catch(reject); + }); + + Alert.alert('Serketî!', `${amount} wUSDT contributed successfully!`); + setContributionAmount(''); + setShowDetailModal(false); + fetchPresales(); + } catch (error: any) { + Alert.alert('Çewtî', error.message || 'Contribution failed.'); + } finally { + setContributing(false); + } + }, + }, + ] + ); + }; + + // Handle refund + const handleRefund = async (presaleId: number) => { + if (!api || !selectedAccount) return; + + const keyPair = await getKeyPair(selectedAccount.address); + if (!keyPair) { + Alert.alert('Çewtî', 'Could not get keyPair.'); + return; + } + + Alert.alert( + 'Refund', + 'Are you sure you want to refund? Fees may apply.', + [ + { text: 'Betal', style: 'cancel' }, + { + text: 'Refund', + style: 'destructive', + onPress: async () => { + try { + await new Promise((resolve, reject) => { + api.tx.presale + .refund(presaleId) + .signAndSend(keyPair, { nonce: -1 }, ({ status, dispatchError }) => { + if (status.isInBlock || status.isFinalized) { + 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())); + } + } else { + resolve(); + } + } + }) + .catch(reject); + }); + + Alert.alert('Serketî!', 'Refund processed successfully!'); + fetchPresales(); + } catch (error: any) { + Alert.alert('Çewtî', error.message || 'Refund failed.'); + } + }, + }, + ] + ); + }; + + // Contact for new project + const handleNewProject = () => { + Alert.alert( + '🚀 Projeya Nû', + 'Ji bo ku projeya xwe li ser Pezkuwi Launchpad zêde bikin:\n\n📧 team@pezkuwichain.io', + [ + { text: 'Paşê', style: 'cancel' }, + { + text: 'Email Bişîne', + onPress: () => Linking.openURL('mailto:team@pezkuwichain.io?subject=Launchpad%20Project%20Submission'), + }, + ] + ); + }; + + // Render presale card + const renderPresaleCard = ({ item }: { item: PresaleConfig }) => { + const userContrib = userContributions[item.id]; + + return ( + { + setSelectedPresale(item); + setShowDetailModal(true); + }} + activeOpacity={0.7} + > + + + + {item.status} + + #{item.id} + + + Presale #{item.id} + + {formatAmount(item.tokensForSale, 12)} tokens for sale + + + {/* Progress */} + + + + {formatAmount(item.totalRaised)} / {formatAmount(item.limits.hardCap)} + + {item.progress.toFixed(1)}% + + + + {/* Soft cap marker */} + + + + + {/* Stats */} + + + {item.contributorCount} + Beşdar + + {item.status === 'Active' && ( + + + {item.timeRemaining.days}d {item.timeRemaining.hours}h + + Mayî + + )} + {userContrib && !userContrib.refunded && ( + + + {formatAmount(userContrib.amount)} + + Your + + )} + + + {item.accessControl === 'Whitelist' && ( + + 🔒 Whitelist + + )} + + ); + }; + + if (loading) { + return ( + + + + Loading presales... + + + ); + } + + return ( + + + + {/* Header */} + + navigation.goBack()} style={styles.backButton}> + + + + 🚀 Launchpad + Multi-Presale Platform + + + + + + + + {/* Platform Stats */} + + + {presales.length} + Presales + + + + {presales.filter(p => p.status === 'Active').length} + + Active + + + {PLATFORM_FEE_PERCENT}% + Fee + + + + {/* Presales List */} + {presales.length === 0 ? ( + + 🚀 + Presale Tune + + Hîn presale tune. Ji bo ku projeya xwe zêde bikin bi me re têkilî daynin. + + + Projeya Nû Zêde Bike + + + ) : ( + item.id.toString()} + contentContainerStyle={styles.listContent} + refreshControl={} + showsVerticalScrollIndicator={false} + /> + )} + + {/* Detail Modal */} + setShowDetailModal(false)} + > + {selectedPresale && ( + + + setShowDetailModal(false)}> + + + Presale #{selectedPresale.id} + + + + + {/* Status */} + + {selectedPresale.status} + + + {/* Progress */} + + Progress + + + + + + {formatAmount(selectedPresale.totalRaised)} / {formatAmount(selectedPresale.limits.hardCap)} wUSDT + + {selectedPresale.progress.toFixed(1)}% + + + Soft Cap: {formatAmount(selectedPresale.limits.softCap)} wUSDT + + + + {/* Time Remaining */} + {selectedPresale.status === 'Active' && ( + + Time Remaining + + + {selectedPresale.timeRemaining.days} + Days + + : + + {selectedPresale.timeRemaining.hours} + Hours + + : + + {selectedPresale.timeRemaining.minutes} + Min + + + + )} + + {/* Contribution Limits */} + + Contribution Limits + + Min: + {formatAmount(selectedPresale.limits.minContribution)} wUSDT + + + Max: + {formatAmount(selectedPresale.limits.maxContribution)} wUSDT + + + Contributors: + {selectedPresale.contributorCount} + + + + {/* Refund Info */} + + Refund Policy + + Grace Period Fee: + {selectedPresale.graceRefundFeePercent}% + + + Normal Fee: + {selectedPresale.refundFeePercent}% + + + + {/* Your Contribution */} + {userContributions[selectedPresale.id] && ( + + Your Contribution + + + {formatAmount(userContributions[selectedPresale.id].amount)} wUSDT + + {userContributions[selectedPresale.id].refunded ? ( + Refunded + ) : selectedPresale.status === 'Active' ? ( + handleRefund(selectedPresale.id)} + > + Refund + + ) : null} + + + )} + + {/* Contribute Form */} + {selectedPresale.status === 'Active' && ( + + Contribute + + {selectedAccount && ( + + Balance: {formatAmount(assetBalances[selectedPresale.paymentAsset] || '0')} wUSDT + + )} + + + + wUSDT + + + + {contributing ? ( + + ) : ( + Contribute + )} + + + + Platform Fee: {PLATFORM_FEE_PERCENT}% (50% treasury, 25% burn, 25% stakers) + + + )} + + + + + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F8F9FA', + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: 12, + fontSize: 14, + color: '#666', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: KurdistanColors.spi, + borderBottomWidth: 1, + borderBottomColor: '#E0E0E0', + }, + backButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#F0F0F0', + justifyContent: 'center', + alignItems: 'center', + }, + backIcon: { + fontSize: 20, + color: '#333', + }, + headerContent: { + flex: 1, + marginLeft: 12, + }, + headerTitle: { + fontSize: 20, + fontWeight: 'bold', + color: KurdistanColors.reş, + }, + headerSubtitle: { + fontSize: 12, + color: '#666', + marginTop: 2, + }, + addButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: KurdistanColors.kesk, + justifyContent: 'center', + alignItems: 'center', + }, + addIcon: { + fontSize: 24, + color: KurdistanColors.spi, + fontWeight: 'bold', + }, + platformStats: { + flexDirection: 'row', + justifyContent: 'space-around', + paddingVertical: 16, + backgroundColor: KurdistanColors.spi, + borderBottomWidth: 1, + borderBottomColor: '#E0E0E0', + }, + platformStatItem: { + alignItems: 'center', + }, + platformStatValue: { + fontSize: 20, + fontWeight: 'bold', + color: KurdistanColors.kesk, + }, + platformStatLabel: { + fontSize: 11, + color: '#888', + marginTop: 2, + }, + listContent: { + padding: 16, + paddingBottom: 40, + }, + presaleCard: { + backgroundColor: KurdistanColors.spi, + borderRadius: 16, + padding: 16, + marginBottom: 16, + boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.08)', + elevation: 3, + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + statusBadge: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + statusDot: { + width: 6, + height: 6, + borderRadius: 3, + marginRight: 6, + }, + statusText: { + fontSize: 11, + fontWeight: '600', + }, + presaleId: { + fontSize: 12, + color: '#888', + }, + tokenName: { + fontSize: 18, + fontWeight: 'bold', + color: KurdistanColors.reş, + marginBottom: 4, + }, + tokenInfo: { + fontSize: 12, + color: '#666', + marginBottom: 12, + }, + progressSection: { + marginBottom: 12, + }, + progressHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 6, + }, + progressLabel: { + fontSize: 11, + color: '#666', + }, + progressPercent: { + fontSize: 11, + fontWeight: '600', + color: KurdistanColors.kesk, + }, + progressBar: { + height: 6, + backgroundColor: '#E0E0E0', + borderRadius: 3, + overflow: 'visible', + position: 'relative', + }, + progressFill: { + height: '100%', + backgroundColor: KurdistanColors.kesk, + borderRadius: 3, + }, + softCapMarker: { + position: 'absolute', + top: -2, + width: 2, + height: 10, + backgroundColor: '#FF9800', + }, + statsRow: { + flexDirection: 'row', + justifyContent: 'flex-start', + gap: 24, + }, + statItem: { + alignItems: 'center', + }, + statValue: { + fontSize: 14, + fontWeight: '600', + color: '#333', + }, + statLabel: { + fontSize: 10, + color: '#888', + }, + whitelistBadge: { + position: 'absolute', + top: 12, + right: 12, + backgroundColor: '#FFF3E0', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 8, + }, + whitelistText: { + fontSize: 10, + color: '#E65100', + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 32, + }, + emptyIcon: { + fontSize: 64, + marginBottom: 16, + }, + emptyTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#333', + marginBottom: 8, + }, + emptyText: { + fontSize: 14, + color: '#666', + textAlign: 'center', + marginBottom: 24, + }, + emptyButton: { + backgroundColor: KurdistanColors.kesk, + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 12, + }, + emptyButtonText: { + color: KurdistanColors.spi, + fontSize: 14, + fontWeight: '600', + }, + // Modal styles + modalContainer: { + flex: 1, + backgroundColor: '#F8F9FA', + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: KurdistanColors.spi, + borderBottomWidth: 1, + borderBottomColor: '#E0E0E0', + }, + modalClose: { + fontSize: 20, + color: '#666', + }, + modalTitle: { + fontSize: 18, + fontWeight: 'bold', + color: KurdistanColors.reş, + }, + modalContent: { + flex: 1, + padding: 16, + }, + detailStatusBadge: { + alignSelf: 'center', + paddingHorizontal: 20, + paddingVertical: 8, + borderRadius: 20, + marginBottom: 20, + }, + detailStatusText: { + color: KurdistanColors.spi, + fontSize: 14, + fontWeight: '600', + }, + detailSection: { + backgroundColor: KurdistanColors.spi, + borderRadius: 12, + padding: 16, + marginBottom: 16, + }, + detailSectionTitle: { + fontSize: 14, + fontWeight: '700', + color: KurdistanColors.reş, + marginBottom: 12, + }, + detailProgressBar: { + height: 8, + backgroundColor: '#E0E0E0', + borderRadius: 4, + marginBottom: 8, + }, + detailProgressInfo: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + detailProgressText: { + fontSize: 12, + color: '#666', + }, + detailProgressPercent: { + fontSize: 12, + fontWeight: '600', + color: KurdistanColors.kesk, + }, + softCapNote: { + fontSize: 11, + color: '#FF9800', + marginTop: 8, + }, + timeGrid: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + timeItem: { + alignItems: 'center', + minWidth: 50, + }, + timeValue: { + fontSize: 24, + fontWeight: 'bold', + color: KurdistanColors.kesk, + }, + timeUnit: { + fontSize: 10, + color: '#888', + }, + timeSeparator: { + fontSize: 20, + fontWeight: 'bold', + color: '#CCC', + marginHorizontal: 8, + }, + detailRow: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 6, + borderBottomWidth: 1, + borderBottomColor: '#F0F0F0', + }, + detailLabel: { + fontSize: 13, + color: '#666', + }, + detailValue: { + fontSize: 13, + fontWeight: '600', + color: '#333', + }, + yourContribution: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + yourContribAmount: { + fontSize: 20, + fontWeight: 'bold', + color: KurdistanColors.kesk, + }, + refundedBadge: { + fontSize: 12, + color: '#999', + backgroundColor: '#F0F0F0', + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 8, + }, + refundButton: { + backgroundColor: '#FFEBEE', + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + }, + refundButtonText: { + color: '#F44336', + fontSize: 12, + fontWeight: '600', + }, + contributeSection: { + backgroundColor: KurdistanColors.spi, + borderRadius: 12, + padding: 16, + marginBottom: 16, + }, + balanceText: { + fontSize: 12, + color: '#666', + marginBottom: 12, + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#F5F5F5', + borderRadius: 12, + paddingHorizontal: 16, + marginBottom: 12, + }, + input: { + flex: 1, + height: 50, + fontSize: 18, + color: '#333', + }, + inputSuffix: { + fontSize: 14, + fontWeight: '600', + color: '#888', + }, + contributeButton: { + backgroundColor: KurdistanColors.kesk, + borderRadius: 12, + paddingVertical: 14, + alignItems: 'center', + marginBottom: 12, + }, + buttonDisabled: { + opacity: 0.6, + }, + contributeButtonText: { + color: KurdistanColors.spi, + fontSize: 16, + fontWeight: '700', + }, + feeNote: { + fontSize: 10, + color: '#999', + textAlign: 'center', + }, +}); + +export default LaunchpadScreen; diff --git a/mobile/src/screens/PerwerdeScreen.tsx b/mobile/src/screens/PerwerdeScreen.tsx new file mode 100644 index 00000000..cb9f6815 --- /dev/null +++ b/mobile/src/screens/PerwerdeScreen.tsx @@ -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('courses'); + const [courses, setCourses] = useState([]); + const [myEnrollments, setMyEnrollments] = useState([]); + const [perwerdeScore, setPerwerdeScore] = useState(0); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [selectedCourse, setSelectedCourse] = useState(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((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) => ( + setActiveTab(tab)} + > + + {labelKu} + + {count > 0 && ( + + + {count} + + + )} + + ); + + // Render course card + const renderCourseCard = (course: Course) => { + const enrolled = isEnrolled(course.id); + const completed = isCompleted(course.id); + const enrollment = getEnrollment(course.id); + + return ( + { + setSelectedCourse(course); + setShowCourseModal(true); + }} + activeOpacity={0.7} + > + + + + {completed ? '✅' : enrolled ? '📖' : '📚'} + + + + {course.name} + Kurs #{course.id} + + {completed && enrollment && ( + + +{enrollment.points_earned} + + )} + + + {course.description} + + + {completed ? ( + Qediya / Completed + ) : enrolled ? ( + Tev li / Enrolled + ) : ( + Amade / Available + )} + + + ); + }; + + // Not connected view + if (!isConnected) { + return ( + + + + + 📚 + Perwerde + + Platforma Perwerdehiya Dijîtal{'\n'}Digital Education Platform + + + Ji kerema xwe wallet ve girêbidin da ku bikaribin kursan bibînin û tev li wan bibin. + + + Please connect your wallet to view and enroll in courses. + + + + + ); + } + + const filteredCourses = getFilteredCourses(); + const enrolledCount = myEnrollments.filter(e => !e.completed_at).length; + const completedCount = myEnrollments.filter(e => e.completed_at).length; + + return ( + + + + {/* Header */} + + navigation.goBack()} + > + + + + Perwerde + Platforma Perwerdehiya Dijîtal + + + + {/* Score Card */} + + + {perwerdeScore} + Puan / Points + + + + {completedCount} + Qediyayî / Done + + + + {enrolledCount} + Aktîv / Active + + + + {/* Tabs */} + + {renderTab('courses', 'Courses', 'Kurs', courses.filter(c => c.status === 'Active').length)} + {renderTab('enrolled', 'Enrolled', 'Tev li', enrolledCount)} + {renderTab('completed', 'Completed', 'Qediya', completedCount)} + + + {/* Content */} + + } + > + {loading ? ( + + + Tê barkirin... / Loading... + + ) : filteredCourses.length === 0 ? ( + + + {activeTab === 'courses' ? '📭' : activeTab === 'enrolled' ? '📋' : '🎓'} + + + {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'} + + + ) : ( + + {filteredCourses.map(renderCourseCard)} + + )} + + + + + {/* Course Detail Modal */} + setShowCourseModal(false)} + > + + + {selectedCourse && ( + <> + + {selectedCourse.name} + setShowCourseModal(false)}> + + + + + + + Danasîn / Description + {selectedCourse.description} + + + + Agahdarî / Info + + Kurs ID: + #{selectedCourse.id} + + + Rewş / Status: + + {selectedCourse.status === 'Active' ? 'Aktîv' : 'Arşîv'} + + + {isEnrolled(selectedCourse.id) && ( + + Têketin / Enrolled: + + {isCompleted(selectedCourse.id) ? 'Qediya ✅' : 'Aktîv 📖'} + + + )} + {isCompleted(selectedCourse.id) && ( + + Puan / Points: + + +{getEnrollment(selectedCourse.id)?.points_earned || 0} + + + )} + + + {selectedCourse.content_link && ( + openContent(selectedCourse.content_link)} + > + 📄 + + Naveroka Kursê Veke / Open Course Content + + + )} + + + + {!isEnrolled(selectedCourse.id) && selectedCourse.status === 'Active' ? ( + handleEnroll(selectedCourse.id)} + disabled={enrolling} + > + {enrolling ? ( + + ) : ( + <> + 📝 + Tev li Kursê / Enroll + + )} + + ) : isCompleted(selectedCourse.id) ? ( + + 🎓 + + Te ev kurs qedand! / You completed this course! + + + ) : ( + + 📖 + + Tu tev li vê kursê yî / You are enrolled + + + )} + + + )} + + + + + ); +}; + +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; diff --git a/mobile/src/screens/PresidentScreen.tsx b/mobile/src/screens/PresidentScreen.tsx new file mode 100644 index 00000000..e280d226 --- /dev/null +++ b/mobile/src/screens/PresidentScreen.tsx @@ -0,0 +1,1368 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + SafeAreaView, + ScrollView, + StatusBar, + ActivityIndicator, + Alert, + FlatList, + Modal, + RefreshControl, +} 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 { + getActiveElections, + getElectionCandidates, + getElectionResults, + hasVoted, + getCurrentBlock, + blocksToTime, + type ElectionInfo, + type CandidateInfo, + type ElectionResult, +} from '../../shared/lib/welati'; + +type TabType = 'history' | 'active' | 'candidate'; + +// Presidential election constants (from pezpallet-welati) +const PRESIDENTIAL_REQUIREMENTS = { + minTrustScore: 600, + minEndorsements: 1000, + depositAmount: '100 PEZ', + requiredTiki: 'Welati', +}; + +const PresidentScreen: React.FC = () => { + const navigation = useNavigation(); + const { api, isApiReady, selectedAccount, getKeyPair } = usePezkuwi(); + + // Access control state + const [hasWelatiTiki, setHasWelatiTiki] = useState(null); + const [checkingAccess, setCheckingAccess] = useState(true); + + // Tab state + const [activeTab, setActiveTab] = useState('active'); + + // Election data + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [currentBlock, setCurrentBlock] = useState(0); + + // Elections + const [activeElections, setActiveElections] = useState([]); + const [pastElections, setPastElections] = useState([]); + const [candidates, setCandidates] = useState([]); + const [selectedElection, setSelectedElection] = useState(null); + const [userHasVoted, setUserHasVoted] = useState(false); + + // Candidate eligibility + const [eligibilityStatus, setEligibilityStatus] = useState<{ + eligible: boolean; + trustScore: number; + reasons: string[]; + } | null>(null); + const [checkingEligibility, setCheckingEligibility] = useState(false); + + // Voting modal + const [showVoteModal, setShowVoteModal] = useState(false); + const [selectedCandidate, setSelectedCandidate] = useState(null); + const [submittingVote, setSubmittingVote] = useState(false); + + // Check welati tiki access + 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('[President] Error checking tiki:', error); + setHasWelatiTiki(false); + } finally { + setCheckingAccess(false); + } + }; + + checkAccess(); + }, [api, isApiReady, selectedAccount]); + + // Fetch election data + const fetchElectionData = useCallback(async () => { + if (!api || !isApiReady) return; + + try { + setLoading(true); + + // Get current block + const block = await getCurrentBlock(api); + setCurrentBlock(block); + + // Get all elections + const elections = await getActiveElections(api); + + // Filter presidential elections + const presidentialElections = elections.filter(e => e.electionType === 'Presidential'); + + // Separate active and completed + const active = presidentialElections.filter(e => e.status !== 'Completed'); + const completed = presidentialElections.filter(e => e.status === 'Completed'); + + setActiveElections(active); + + // Get results for completed elections + const results: ElectionResult[] = []; + for (const election of completed) { + const result = await getElectionResults(api, election.electionId); + if (result) { + results.push(result); + } + } + setPastElections(results); + + // If there's an active election, get candidates + if (active.length > 0) { + const firstActive = active[0]; + setSelectedElection(firstActive); + const electionCandidates = await getElectionCandidates(api, firstActive.electionId); + setCandidates(electionCandidates); + + // Check if user has voted + if (selectedAccount) { + const voted = await hasVoted(api, firstActive.electionId, selectedAccount.address); + setUserHasVoted(voted); + } + } + + } catch (error) { + if (__DEV__) console.error('[President] Error fetching elections:', error); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [api, isApiReady, selectedAccount]); + + useEffect(() => { + if (hasWelatiTiki) { + fetchElectionData(); + } + }, [hasWelatiTiki, fetchElectionData]); + + // Check candidate eligibility + const checkCandidateEligibility = async () => { + if (!api || !isApiReady || !selectedAccount) return; + + setCheckingEligibility(true); + + try { + // Check trust score + const trustScoreRaw = await api.query.trust?.trustScores?.(selectedAccount.address); + const trustScore = trustScoreRaw ? Number(trustScoreRaw.toString()) : 0; + + const reasons: string[] = []; + let eligible = true; + + // Check trust score requirement + if (trustScore < PRESIDENTIAL_REQUIREMENTS.minTrustScore) { + eligible = false; + reasons.push(`Trust score must be at least ${PRESIDENTIAL_REQUIREMENTS.minTrustScore} (yours: ${trustScore})`); + } + + // Check if already a candidate in active election + if (selectedElection) { + const isCandidate = candidates.some(c => c.account === selectedAccount.address); + if (isCandidate) { + eligible = false; + reasons.push('You are already a candidate in this election'); + } + } + + // Check if there's an active election in candidacy period + if (!selectedElection || selectedElection.status !== 'CandidacyPeriod') { + eligible = false; + reasons.push('No election is currently accepting candidates'); + } + + if (eligible) { + reasons.push('You meet all requirements to become a candidate!'); + } + + setEligibilityStatus({ eligible, trustScore, reasons }); + + } catch (error) { + if (__DEV__) console.error('[President] Error checking eligibility:', error); + Alert.alert('Error', 'Failed to check eligibility'); + } finally { + setCheckingEligibility(false); + } + }; + + // Submit vote + const handleVote = async () => { + if (!api || !selectedAccount || !selectedElection || !selectedCandidate) return; + + setSubmittingVote(true); + + try { + const keyPair = await getKeyPair(selectedAccount.address); + if (!keyPair) { + Alert.alert('Error', 'Could not retrieve key pair for signing'); + return; + } + + // Create vote transaction + const tx = api.tx.welati.castVote( + selectedElection.electionId, + [selectedCandidate.account], + null // No district for presidential + ); + + // Sign and send + await tx.signAndSend(keyPair, { nonce: -1 }, ({ status, events }) => { + if (status.isInBlock || status.isFinalized) { + // Check for success + const success = events.some(({ event }) => + api.events.system.ExtrinsicSuccess.is(event) + ); + + if (success) { + Alert.alert( + 'Vote Submitted!', + 'Your vote has been recorded on the blockchain.', + [{ text: 'OK', onPress: () => { + setShowVoteModal(false); + setSelectedCandidate(null); + setUserHasVoted(true); + fetchElectionData(); + }}] + ); + } else { + Alert.alert('Error', 'Vote transaction failed'); + } + setSubmittingVote(false); + } + }); + + } catch (error: any) { + if (__DEV__) console.error('[President] Vote error:', error); + Alert.alert('Error', error.message || 'Failed to submit vote'); + setSubmittingVote(false); + } + }; + + // Register as candidate + const handleRegisterCandidate = async () => { + if (!api || !selectedAccount || !selectedElection) return; + + Alert.alert( + 'Register as Candidate', + `To register as a presidential candidate, you need:\n\n` + + `• Trust Score: ${PRESIDENTIAL_REQUIREMENTS.minTrustScore}+\n` + + `• Endorsements: ${PRESIDENTIAL_REQUIREMENTS.minEndorsements}+\n` + + `• Deposit: ${PRESIDENTIAL_REQUIREMENTS.depositAmount}\n\n` + + `This will open the registration form.`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Continue', + onPress: () => { + // TODO: Implement candidate registration form + Alert.alert('Coming Soon', 'Candidate registration will be available soon.'); + } + } + ] + ); + }; + + // Format time remaining + const formatTimeRemaining = (endBlock: number) => { + const remaining = endBlock - currentBlock; + if (remaining <= 0) return 'Ended'; + const time = blocksToTime(remaining); + if (time.days > 0) return `${time.days}d ${time.hours}h`; + if (time.hours > 0) return `${time.hours}h ${time.minutes}m`; + return `${time.minutes}m`; + }; + + // Format address + const formatAddress = (address: string) => { + return `${address.slice(0, 6)}...${address.slice(-4)}`; + }; + + // Render access denied screen + if (checkingAccess) { + return ( + + + + + Checking access... + + + ); + } + + if (!hasWelatiTiki) { + return ( + + + + + 🏛️ + Citizenship Required + + Pêdivî ye ku hûn welatî bin da ku bikarin beşdarî hilbijartinê bibin + + + You must be a citizen to participate in presidential elections. + Please complete your citizenship application first. + + + navigation.navigate('BeCitizen' as never)} + > + Become a Citizen + + + navigation.goBack()} + > + ← Go Back + + + + + ); + } + + // Main content + return ( + + + + {/* Header */} + + navigation.goBack()} style={styles.headerBackButton}> + + + + + 👑 + Serokî / President + Hilbijartina Serokê Komarê + + + {/* Kurdistan Sun decoration */} + + + {[...Array(21)].map((_, i) => ( + + ))} + + + + {/* Tab Navigation */} + + setActiveTab('history')} + > + + Dîrok + + + History + + + + setActiveTab('active')} + > + + Hilbijartin + + + Elections + + + + setActiveTab('candidate')} + > + + Berendam + + + Candidate + + + + + {/* Content */} + { + setRefreshing(true); + fetchElectionData(); + }} /> + } + > + {loading ? ( + + + Loading elections... + + ) : ( + <> + {/* History Tab */} + {activeTab === 'history' && ( + + Past Presidential Elections + + {pastElections.length === 0 ? ( + + 📜 + No past elections yet + + Presidential election history will appear here + + + ) : ( + pastElections.map((result, index) => ( + + + Election #{result.electionId} + + Completed + + + + + Winner (Serok) + + 👑 {result.winners[0] ? formatAddress(result.winners[0]) : 'N/A'} + + + + + + {result.totalVotes} + Total Votes + + + {result.turnoutPercentage}% + Turnout + + + + {result.runoffRequired ? 'Yes' : 'No'} + + Runoff + + + + )) + )} + + )} + + {/* Active Elections Tab */} + {activeTab === 'active' && ( + + {activeElections.length === 0 ? ( + + 🗳️ + No active elections + + Presidential elections will appear here when initiated + + + ) : ( + <> + {/* Election Info Card */} + {selectedElection && ( + + + + Presidential Election #{selectedElection.electionId} + + + + {selectedElection.status === 'VotingPeriod' ? '🗳️ Voting Open' : + selectedElection.status === 'CandidacyPeriod' ? '📝 Registration Open' : + selectedElection.status === 'CampaignPeriod' ? '📢 Campaign Period' : + 'Completed'} + + + + + + + + + {selectedElection.totalCandidates} + + Candidates + + + + {selectedElection.totalVotes} + + Votes Cast + + + + {formatTimeRemaining(selectedElection.votingEndBlock)} + + Time Left + + + + {userHasVoted && ( + + ✓ You have voted + + )} + + + )} + + {/* Candidates List */} + Candidates + + {candidates.length === 0 ? ( + + + No candidates registered yet + + + ) : ( + candidates.map((candidate, index) => ( + { + if (!userHasVoted && selectedElection?.status === 'VotingPeriod') { + setSelectedCandidate(candidate); + setShowVoteModal(true); + } + }} + disabled={userHasVoted || selectedElection?.status !== 'VotingPeriod'} + > + + + {index === 0 ? '👑' : `#${index + 1}`} + + + + + + {formatAddress(candidate.account)} + + + {candidate.endorsersCount} endorsements + + + + + {candidate.voteCount} + votes + + + {!userHasVoted && selectedElection?.status === 'VotingPeriod' && ( + + Vote + + )} + + )) + )} + + )} + + )} + + {/* Become Candidate Tab */} + {activeTab === 'candidate' && ( + + + Become a Presidential Candidate + + Bibin berendamê serokîtiyê + + + + Requirements: + + + + + Welati Tiki (Citizenship) + + + + + + 📊 + + Trust Score: {PRESIDENTIAL_REQUIREMENTS.minTrustScore}+ + + + {eligibilityStatus ? + (eligibilityStatus.trustScore >= PRESIDENTIAL_REQUIREMENTS.minTrustScore ? '✅' : '❌') + : '❓'} + + + + + 👥 + + {PRESIDENTIAL_REQUIREMENTS.minEndorsements}+ Endorsements + + + + + + 💰 + + Deposit: {PRESIDENTIAL_REQUIREMENTS.depositAmount} + + + + + + + {checkingEligibility ? ( + + ) : ( + Check My Eligibility + )} + + + {eligibilityStatus && ( + + + {eligibilityStatus.eligible ? '✅ You are eligible!' : '❌ Not eligible yet'} + + {eligibilityStatus.reasons.map((reason, i) => ( + • {reason} + ))} + + {eligibilityStatus.eligible && selectedElection?.status === 'CandidacyPeriod' && ( + + Register as Candidate + + )} + + )} + + + )} + + )} + + + {/* Vote Confirmation Modal */} + setShowVoteModal(false)} + > + + + Confirm Your Vote + + Dengê xwe piştrast bikin + + + {selectedCandidate && ( + + Voting for: + + {formatAddress(selectedCandidate.account)} + + + )} + + + ⚠️ Your vote cannot be changed after submission. + + + + { + setShowVoteModal(false); + setSelectedCandidate(null); + }} + > + Cancel + + + + {submittingVote ? ( + + ) : ( + Confirm Vote + )} + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F5F5F5', + }, + + // Loading + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + color: KurdistanColors.spi, + marginTop: 16, + fontSize: 16, + }, + + // Access Denied + 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, + }, + + // Header + header: { + paddingTop: 20, + paddingBottom: 30, + paddingHorizontal: 20, + position: 'relative', + overflow: 'hidden', + }, + headerBackButton: { + position: 'absolute', + top: 20, + left: 16, + zIndex: 10, + padding: 8, + }, + headerBackText: { + fontSize: 24, + color: KurdistanColors.spi, + }, + headerContent: { + alignItems: 'center', + marginTop: 10, + }, + headerIcon: { + fontSize: 48, + marginBottom: 8, + }, + headerTitle: { + fontSize: 24, + fontWeight: 'bold', + color: KurdistanColors.spi, + marginBottom: 4, + }, + headerSubtitle: { + fontSize: 14, + color: 'rgba(255,255,255,0.8)', + }, + + // Sun decoration + sunDecoration: { + position: 'absolute', + right: -30, + top: -30, + width: 120, + height: 120, + opacity: 0.2, + }, + sunCenter: { + position: 'absolute', + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: KurdistanColors.zer, + left: 40, + top: 40, + }, + sunRay: { + position: 'absolute', + width: 3, + height: 60, + backgroundColor: KurdistanColors.zer, + left: 58, + top: 0, + transformOrigin: 'center bottom', + }, + + // Tabs + tabContainer: { + flexDirection: 'row', + backgroundColor: KurdistanColors.spi, + paddingVertical: 8, + paddingHorizontal: 8, + borderBottomWidth: 1, + borderBottomColor: '#E0E0E0', + }, + tab: { + flex: 1, + paddingVertical: 12, + alignItems: 'center', + borderRadius: 8, + }, + tabActive: { + backgroundColor: `${KurdistanColors.kesk}15`, + }, + tabText: { + fontSize: 14, + fontWeight: '600', + color: '#666', + }, + tabTextActive: { + color: KurdistanColors.kesk, + }, + tabSubtext: { + fontSize: 10, + color: '#999', + marginTop: 2, + }, + tabSubtextActive: { + color: KurdistanColors.kesk, + }, + + // Content + content: { + flex: 1, + }, + tabContent: { + padding: 16, + }, + + // Loading section + loadingSection: { + padding: 60, + alignItems: 'center', + }, + loadingSectionText: { + marginTop: 12, + color: '#666', + }, + + // Section title + sectionTitle: { + fontSize: 18, + fontWeight: 'bold', + color: KurdistanColors.reş, + marginBottom: 16, + }, + + // Empty state + emptyState: { + alignItems: 'center', + padding: 40, + }, + emptyStateIcon: { + fontSize: 64, + marginBottom: 16, + }, + emptyStateText: { + fontSize: 18, + fontWeight: '600', + color: '#666', + marginBottom: 8, + }, + emptyStateSubtext: { + fontSize: 14, + color: '#999', + textAlign: 'center', + }, + + // History card + historyCard: { + backgroundColor: KurdistanColors.spi, + borderRadius: 16, + padding: 16, + marginBottom: 16, + boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.08)', + elevation: 3, + }, + historyHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + historyTitle: { + fontSize: 16, + fontWeight: 'bold', + color: KurdistanColors.reş, + }, + historyBadge: { + backgroundColor: '#E8F5E9', + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + historyBadgeText: { + color: KurdistanColors.kesk, + fontSize: 12, + fontWeight: '600', + }, + historyWinner: { + backgroundColor: `${KurdistanColors.zer}20`, + padding: 12, + borderRadius: 12, + marginBottom: 12, + }, + historyWinnerLabel: { + fontSize: 12, + color: '#666', + marginBottom: 4, + }, + historyWinnerAddress: { + fontSize: 16, + fontWeight: 'bold', + color: KurdistanColors.reş, + }, + historyStats: { + flexDirection: 'row', + justifyContent: 'space-around', + }, + historyStat: { + alignItems: 'center', + }, + historyStatValue: { + fontSize: 18, + fontWeight: 'bold', + color: KurdistanColors.kesk, + }, + historyStatLabel: { + fontSize: 12, + color: '#666', + }, + + // Election card + electionCard: { + borderRadius: 16, + overflow: 'hidden', + marginBottom: 24, + boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)', + elevation: 5, + }, + electionCardHeader: { + padding: 20, + alignItems: 'center', + }, + electionCardTitle: { + fontSize: 18, + fontWeight: 'bold', + color: KurdistanColors.spi, + marginBottom: 8, + }, + electionStatusBadge: { + backgroundColor: 'rgba(255,255,255,0.2)', + paddingHorizontal: 16, + paddingVertical: 6, + borderRadius: 20, + }, + electionStatusText: { + color: KurdistanColors.spi, + fontSize: 14, + fontWeight: '600', + }, + electionCardBody: { + backgroundColor: KurdistanColors.spi, + padding: 20, + }, + electionStats: { + flexDirection: 'row', + justifyContent: 'space-around', + }, + electionStat: { + alignItems: 'center', + }, + electionStatValue: { + fontSize: 24, + fontWeight: 'bold', + color: KurdistanColors.kesk, + }, + electionStatLabel: { + fontSize: 12, + color: '#666', + marginTop: 4, + }, + votedBadge: { + backgroundColor: '#E8F5E9', + padding: 12, + borderRadius: 12, + marginTop: 16, + alignItems: 'center', + }, + votedBadgeText: { + color: KurdistanColors.kesk, + fontWeight: '600', + }, + + // Candidates + noCandidates: { + padding: 24, + alignItems: 'center', + }, + noCandidatesText: { + color: '#999', + fontSize: 14, + }, + candidateCard: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: KurdistanColors.spi, + borderRadius: 12, + padding: 16, + marginBottom: 12, + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.05)', + elevation: 2, + }, + candidateCardLeader: { + borderWidth: 2, + borderColor: KurdistanColors.zer, + }, + candidateRank: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#F0F0F0', + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, + candidateRankText: { + fontSize: 16, + fontWeight: 'bold', + color: KurdistanColors.reş, + }, + candidateInfo: { + flex: 1, + }, + candidateAddress: { + fontSize: 14, + fontWeight: '600', + color: KurdistanColors.reş, + fontFamily: 'monospace', + }, + candidateEndorsements: { + fontSize: 12, + color: '#666', + marginTop: 2, + }, + candidateVotes: { + alignItems: 'center', + marginRight: 12, + }, + candidateVoteCount: { + fontSize: 20, + fontWeight: 'bold', + color: KurdistanColors.kesk, + }, + candidateVoteLabel: { + fontSize: 10, + color: '#666', + }, + voteButton: { + backgroundColor: KurdistanColors.kesk, + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + }, + voteButtonText: { + color: KurdistanColors.spi, + fontWeight: 'bold', + fontSize: 14, + }, + + // Candidate info card + candidateInfoCard: { + backgroundColor: KurdistanColors.spi, + borderRadius: 16, + padding: 24, + boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.08)', + elevation: 4, + }, + candidateInfoTitle: { + fontSize: 20, + fontWeight: 'bold', + color: KurdistanColors.reş, + marginBottom: 4, + }, + candidateInfoSubtitle: { + fontSize: 14, + color: KurdistanColors.kesk, + marginBottom: 24, + fontStyle: 'italic', + }, + requirementsList: { + marginBottom: 24, + }, + requirementsTitle: { + fontSize: 16, + fontWeight: '600', + color: KurdistanColors.reş, + marginBottom: 16, + }, + requirementItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: '#F0F0F0', + }, + requirementIcon: { + fontSize: 20, + marginRight: 12, + }, + requirementText: { + flex: 1, + fontSize: 14, + color: KurdistanColors.reş, + }, + requirementStatus: { + fontSize: 20, + }, + checkEligibilityButton: { + backgroundColor: KurdistanColors.kesk, + padding: 16, + borderRadius: 12, + alignItems: 'center', + }, + checkEligibilityText: { + color: KurdistanColors.spi, + fontSize: 16, + fontWeight: 'bold', + }, + eligibilityResult: { + marginTop: 20, + padding: 16, + borderRadius: 12, + }, + eligibilityResultSuccess: { + backgroundColor: '#E8F5E9', + }, + eligibilityResultFail: { + backgroundColor: '#FFEBEE', + }, + eligibilityResultTitle: { + fontSize: 16, + fontWeight: 'bold', + marginBottom: 12, + }, + eligibilityResultReason: { + fontSize: 14, + color: '#666', + marginBottom: 4, + }, + registerButton: { + backgroundColor: KurdistanColors.zer, + padding: 14, + borderRadius: 10, + alignItems: 'center', + marginTop: 16, + }, + registerButtonText: { + color: KurdistanColors.reş, + fontSize: 16, + fontWeight: 'bold', + }, + + // Modal + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'flex-end', + }, + modalContent: { + backgroundColor: KurdistanColors.spi, + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + padding: 24, + }, + modalTitle: { + fontSize: 22, + fontWeight: 'bold', + color: KurdistanColors.reş, + textAlign: 'center', + marginBottom: 4, + }, + modalSubtitle: { + fontSize: 14, + color: KurdistanColors.kesk, + textAlign: 'center', + marginBottom: 24, + }, + modalCandidate: { + backgroundColor: '#F5F5F5', + padding: 16, + borderRadius: 12, + marginBottom: 16, + }, + modalCandidateLabel: { + fontSize: 12, + color: '#666', + marginBottom: 4, + }, + modalCandidateAddress: { + fontSize: 16, + fontWeight: 'bold', + color: KurdistanColors.reş, + fontFamily: 'monospace', + }, + modalWarning: { + fontSize: 14, + color: KurdistanColors.sor, + textAlign: 'center', + marginBottom: 24, + }, + modalButtons: { + flexDirection: 'row', + gap: 12, + }, + modalCancelButton: { + flex: 1, + padding: 16, + borderRadius: 12, + backgroundColor: '#F0F0F0', + alignItems: 'center', + }, + modalCancelText: { + fontSize: 16, + fontWeight: '600', + color: '#666', + }, + modalConfirmButton: { + flex: 1, + padding: 16, + borderRadius: 12, + backgroundColor: KurdistanColors.kesk, + alignItems: 'center', + }, + modalConfirmText: { + fontSize: 16, + fontWeight: 'bold', + color: KurdistanColors.spi, + }, +}); + +export default PresidentScreen; diff --git a/mobile/src/screens/ProposalsScreen.tsx b/mobile/src/screens/ProposalsScreen.tsx new file mode 100644 index 00000000..50a09fc7 --- /dev/null +++ b/mobile/src/screens/ProposalsScreen.tsx @@ -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(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 ( + + + + + Checking access... + + + ); + } + + // Access denied - no welati tiki + if (!hasWelatiTiki) { + return ( + + + + + 📜 + Citizenship Required + + Pêdivî ye ku hûn welatî bin da ku bikarin pêşniyarên qanûnî bibînin + + + You must be a citizen to view and participate in law proposals. + Please complete your citizenship application first. + + + navigation.navigate('BeCitizen' as never)} + > + Become a Citizen + + + navigation.goBack()} + > + ← Go Back + + + + + ); + } + + // Access granted - show WebView + return ( + + + + ); +}; + +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; diff --git a/mobile/src/screens/ReferralScreen.tsx b/mobile/src/screens/ReferralScreen.tsx index 8d586f63..7bd30bb0 100644 --- a/mobile/src/screens/ReferralScreen.tsx +++ b/mobile/src/screens/ReferralScreen.tsx @@ -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([]); 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 = () => { - 📋 - Copy + 📱 + QR Code 📤 - Share + Paylaş / Share @@ -430,6 +536,117 @@ const ReferralScreen: React.FC = () => { )} + + {/* Share Sheet Modal */} + setShowShareSheet(false)} + > + setShowShareSheet(false)} + > + + + Paylaş / Share + + Arkadaşlarını davet et, ödül kazan!{'\n'}Invite friends, earn rewards! + + + {/* Share Link Preview */} + + + {referralLink} + + + + {/* Platform Grid */} + + {SHARE_OPTIONS.map((option) => ( + handleShareViaPlatform(option.id)} + activeOpacity={0.7} + > + + {option.icon} + + {option.name} + + ))} + + + {/* Cancel Button */} + setShowShareSheet(false)} + > + İptal / Cancel + + + + + + {/* QR Code Modal */} + setShowQRModal(false)} + > + setShowQRModal(false)} + > + + + Referral QR Kodu + setShowQRModal(false)}> + + + + + + {selectedAccount && ( + + )} + + + + Bu QR kodu arkadaşlarınla paylaş.{'\n'} + Taratarak Pezkuwi'ye katılabilirler. + + + Share this QR code with friends.{'\n'} + They can scan to join Pezkuwi. + + + + {referralCode} + + + { + setShowQRModal(false); + openShareSheet(); + }} + > + 📤 Linki Paylaş / Share Link + + + + ); }; @@ -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; diff --git a/mobile/src/screens/TaxZekatScreen.tsx b/mobile/src/screens/TaxZekatScreen.tsx new file mode 100644 index 00000000..1e8dcb7e --- /dev/null +++ b/mobile/src/screens/TaxZekatScreen.tsx @@ -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('zekat'); + const [amount, setAmount] = useState(''); + const [allocations, setAllocations] = useState(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((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) => ( + + + {item.icon} + + {item.nameKu} + {item.nameEn} + + + + 0 ? String(item.percentage) : ''} + onChangeText={(value) => updateAllocation(item.id, value)} + keyboardType="number-pad" + placeholder="0" + placeholderTextColor="#999" + maxLength={3} + /> + % + + {item.percentage > 0 && parseFloat(amount) > 0 && ( + + {calculateAllocationAmount(item.percentage)} HEZ + + )} + + ); + + return ( + + + + {/* Header */} + + navigation.goBack()} style={styles.backButton}> + ← Paş + + Bac û Zekat + Tax & Zekat + + + + {/* Description */} + + + Beşdariya xwe ya bi dilxwazî ji Komara Dijitaliya Kurdistanê re bişînin. + + + Send your voluntary contribution to the Digital Kurdistan Republic. + + + + {/* Type Selection */} + + Cureyê Beşdariyê / Contribution Type + + setContributionType('zekat')} + > + ☪️ + + Zekat + + + İslami Zekat + + + + setContributionType('tax')} + > + 📜 + + Bac + + + Vergi / Tax + + + + + + {/* Amount Input */} + + Miqdar / Amount + + + HEZ + + {selectedAccount && ( + + Bakiye / Balance: -- HEZ + + )} + + + {/* Allocation Section */} + + + Dabeşkirina Fonê / Fund Allocation + 100 && styles.totalBadgeInvalid, + ]}> + 100 && styles.totalTextInvalid, + ]}> + {totalPercentage}% + + + + + Divê bêkêmasî %100 be / Must equal exactly 100% + + + + {allocations.map(renderAllocationItem)} + + + + {/* Terms Section */} + + + + {contributionType === 'zekat' ? '☪️' : '📜'} + + SOZNAME / COMMITMENT + + {contributionType === 'zekat' ? ( + <> + + 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. + + + 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. + + + ) : ( + <> + + 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. + + + Acil durumlar veya zorunlu hallerde, devlet küçük inisiyatifler kullanabilir. Tüm harcamalar blockchain üzerinde şeffaf olarak kaydedilecektir. + + + )} + + + setTermsAccepted(!termsAccepted)} + > + + {termsAccepted && } + + + Okudum ve kabul ediyorum / I have read and accept + + + + + {/* Submit Button */} + + {isSubmitting ? ( + + ) : ( + + {contributionType === 'zekat' ? '☪️ ZEKAT BIŞÎNE' : '📤 BAC BIŞÎNE'} + + )} + + + + + + {/* Confirmation Modal */} + setShowConfirmModal(false)} + > + + + Piştrast bike / Confirm + + + + Cure / Type: + + {contributionType === 'zekat' ? 'Zekat' : 'Bac / Tax'} + + + + Miqdar / Amount: + {amount} HEZ + + + Dabeşkirin / Allocation: + {allocations + .filter(a => a.percentage > 0) + .map(a => ( + + + {a.icon} {a.nameKu} + + + {calculateAllocationAmount(a.percentage)} HEZ ({a.percentage}%) + + + ))} + + + + setShowConfirmModal(false)} + > + Betal / Cancel + + + ✓ Piştrast + + + + + + + ); +}; + +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; diff --git a/mobile/src/screens/ValidatorsScreen.tsx b/mobile/src/screens/ValidatorsScreen.tsx new file mode 100644 index 00000000..920d3426 --- /dev/null +++ b/mobile/src/screens/ValidatorsScreen.tsx @@ -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(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 ( + + + + + Checking access... + + + ); + } + + // Access denied - no welati tiki + if (!hasWelatiTiki) { + return ( + + + + + 🛡️ + Citizenship Required + + Pêdivî ye ku hûn welatî bin da ku bikarin rastkerên torê bibînin + + + You must be a citizen to access network validators. + Please complete your citizenship application first. + + + navigation.navigate('BeCitizen' as never)} + > + Become a Citizen + + + navigation.goBack()} + > + ← Go Back + + + + + ); + } + + // Access granted - show WebView + return ( + + + + ); +}; + +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; diff --git a/mobile/src/screens/VoteScreen.tsx b/mobile/src/screens/VoteScreen.tsx new file mode 100644 index 00000000..8464e7d3 --- /dev/null +++ b/mobile/src/screens/VoteScreen.tsx @@ -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(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 ( + + + + + Checking access... + + + ); + } + + // Access denied - no welati tiki + if (!hasWelatiTiki) { + return ( + + + + + 🗳️ + Citizenship Required + + Pêdivî ye ku hûn welatî bin da ku bikarin deng bidin + + + You must be a citizen to participate in voting. + Please complete your citizenship application first. + + + navigation.navigate('BeCitizen' as never)} + > + Become a Citizen + + + navigation.goBack()} + > + ← Go Back + + + + + ); + } + + // Access granted - show WebView + return ( + + + + ); +}; + +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; diff --git a/mobile/src/screens/__tests__/__snapshots__/BeCitizenScreen.test.tsx.snap b/mobile/src/screens/__tests__/__snapshots__/BeCitizenScreen.test.tsx.snap index c6e046a5..d4750d32 100644 --- a/mobile/src/screens/__tests__/__snapshots__/BeCitizenScreen.test.tsx.snap +++ b/mobile/src/screens/__tests__/__snapshots__/BeCitizenScreen.test.tsx.snap @@ -4,419 +4,228 @@ exports[`BeCitizenScreen should match snapshot 1`] = ` - - - - + Be Citizen + + + - - - 🏛️ - - - - Be a Citizen - - - Join the Pezkuwi decentralized nation - - - - - - 📝 - - - New Citizen - - - Apply for citizenship and join our community - - - - - 🔐 - - - Existing Citizen - - - Access your citizenship account - - - - - - Citizenship Benefits - - - - ✓ - - - Voting rights in governance - - - - - ✓ - - - Access to exclusive services - - - - - ✓ - - - Referral rewards program - - - - - ✓ - - - Community recognition - - - + Reload + - - + + + + + + Loading... + + + `; diff --git a/mobile/src/screens/__tests__/__snapshots__/DashboardScreen.test.tsx.snap b/mobile/src/screens/__tests__/__snapshots__/DashboardScreen.test.tsx.snap index 0399b92a..df1c2782 100644 --- a/mobile/src/screens/__tests__/__snapshots__/DashboardScreen.test.tsx.snap +++ b/mobile/src/screens/__tests__/__snapshots__/DashboardScreen.test.tsx.snap @@ -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`] = ` > 📊 - - - 🔒 - - - Tax + Bac/Zekat @@ -1807,26 +1781,6 @@ exports[`DashboardScreen should match snapshot 1`] = ` > 👑 - - - 🔒 - - @@ -2019,26 +1970,6 @@ exports[`DashboardScreen should match snapshot 1`] = ` > 🗳️ - - - 🔒 - - @@ -2122,26 +2050,6 @@ exports[`DashboardScreen should match snapshot 1`] = ` > 🛡️ - - - 🔒 - - @@ -2328,26 +2233,6 @@ exports[`DashboardScreen should match snapshot 1`] = ` > 📜 - - - 🔒 - - @@ -2863,26 +2745,6 @@ exports[`DashboardScreen should match snapshot 1`] = ` } } /> - - - 🔒 - - - - - - 📜 - - - - 🔒 - - - - - Library - - - - - - 🗣️ - - - - 🔒 - - - - - Language - - - - - - 🧸 - - - - 🔒 - - - - - Kids - - - - - - 🏺 - - - - 🔒 - - - - - History - - - 📋 + 📱 - Copy + QR Code - Share + Paylaş / Share diff --git a/mobile/src/utils/citizenship.ts b/mobile/src/utils/citizenship.ts new file mode 100644 index 00000000..e6c669ab --- /dev/null +++ b/mobile/src/utils/citizenship.ts @@ -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 { + 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((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((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' }; + } +}