diff --git a/mobile/app.json b/mobile/app.json index 766375de..df8b7a87 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -1,7 +1,7 @@ { "expo": { - "name": "Pezkuwi", - "slug": "pezkuwi", + "name": "PezkuwiApp", + "slug": "pezkuwiapp", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", diff --git a/mobile/index.ts b/mobile/index.ts index d1b6581c..58a7caa3 100644 --- a/mobile/index.ts +++ b/mobile/index.ts @@ -13,8 +13,8 @@ if (__DEV__) console.warn('📦 [INDEX] Setting up Buffer...'); import { Buffer } from 'buffer'; // Global polyfills for Polkadot.js -// @ts-expect-error Global Buffer assignment for polyfill -global.Buffer = Buffer; +// Global Buffer assignment for polyfill +global.Buffer = global.Buffer || require('buffer').Buffer; if (__DEV__) console.warn('✅ [INDEX] Buffer configured'); // TextEncoder/TextDecoder polyfill @@ -22,10 +22,10 @@ if (__DEV__) console.warn('📦 [INDEX] Setting up TextEncoder/TextDecoder...'); if (typeof global.TextEncoder === 'undefined') { // eslint-disable-next-line @typescript-eslint/no-require-imports const { TextEncoder, TextDecoder } = require('text-encoding'); - // @ts-expect-error Global TextEncoder assignment for polyfill - global.TextEncoder = TextEncoder; - // @ts-expect-error Global TextDecoder assignment for polyfill - global.TextDecoder = TextDecoder; + // Global TextEncoder assignment for polyfill + global.TextEncoder = require('text-encoding').TextEncoder; + // Global TextDecoder assignment for polyfill + global.TextDecoder = require('text-encoding').TextDecoder; if (__DEV__) console.warn('✅ [INDEX] TextEncoder/TextDecoder configured'); } else { if (__DEV__) console.warn('ℹ️ [INDEX] TextEncoder/TextDecoder already available'); diff --git a/mobile/metro.config.cjs b/mobile/metro.config.cjs index 25d01fc3..7facd236 100644 --- a/mobile/metro.config.cjs +++ b/mobile/metro.config.cjs @@ -16,8 +16,8 @@ const projectRoot = __dirname; // ============================================ // CUSTOM MODULE RESOLUTION // ============================================ -// DISABLED: Custom resolver causes empty-module.js resolution issues -// Using npm packages directly instead +// Note: @pezkuwi packages have incorrect main field in npm (cjs/cjs/index.js) +// Fixed via postinstall script or manual patch // ============================================ // FILE EXTENSIONS diff --git a/mobile/package.json b/mobile/package.json index a00f5483..0d46675d 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -32,9 +32,9 @@ }, "dependencies": { "@babel/runtime": "^7.28.4", - "@pezkuwi/api": "^16.5.9", + "@pezkuwi/api": "^16.5.11", "@pezkuwi/keyring": "14.0.11", - "@pezkuwi/types": "16.5.9", + "@pezkuwi/types": "^16.5.11", "@pezkuwi/util": "14.0.11", "@pezkuwi/util-crypto": "14.0.11", "@react-native-async-storage/async-storage": "^2.2.0", @@ -74,17 +74,17 @@ }, "overrides": { "react-test-renderer": "19.1.0", - "@pezkuwi/api-augment": "16.5.9", - "@pezkuwi/api-base": "16.5.9", - "@pezkuwi/api-derive": "16.5.9", - "@pezkuwi/rpc-augment": "16.5.9", - "@pezkuwi/rpc-core": "16.5.9", - "@pezkuwi/rpc-provider": "16.5.9", - "@pezkuwi/types": "16.5.9", - "@pezkuwi/types-augment": "16.5.9", - "@pezkuwi/types-codec": "16.5.9", - "@pezkuwi/types-create": "16.5.9", - "@pezkuwi/types-known": "16.5.9", + "@pezkuwi/api-augment": "^16.5.11", + "@pezkuwi/api-base": "^16.5.11", + "@pezkuwi/api-derive": "^16.5.11", + "@pezkuwi/rpc-augment": "^16.5.11", + "@pezkuwi/rpc-core": "^16.5.11", + "@pezkuwi/rpc-provider": "^16.5.11", + "@pezkuwi/types": "^16.5.11", + "@pezkuwi/types-augment": "^16.5.11", + "@pezkuwi/types-codec": "^16.5.11", + "@pezkuwi/types-create": "^16.5.11", + "@pezkuwi/types-known": "^16.5.11", "@pezkuwi/networks": "14.0.11", "@pezkuwi/keyring": "14.0.11", "@pezkuwi/util": "14.0.11", @@ -92,7 +92,6 @@ }, "devDependencies": { "@expo/ngrok": "^4.1.0", - "react-test-renderer": "19.1.0", "@pezkuwi/extension-dapp": "^0.62.20", "@pezkuwi/extension-inject": "^0.62.20", "@testing-library/jest-native": "^5.4.3", @@ -111,6 +110,7 @@ "globals": "^16.5.0", "jest": "^29.7.0", "jest-expo": "^54.0.13", + "react-test-renderer": "19.1.0", "typescript": "~5.9.2", "typescript-eslint": "^8.47.0" }, diff --git a/mobile/src/components/AvatarPickerModal.tsx b/mobile/src/components/AvatarPickerModal.tsx index b4a0790c..5856c185 100644 --- a/mobile/src/components/AvatarPickerModal.tsx +++ b/mobile/src/components/AvatarPickerModal.tsx @@ -9,6 +9,7 @@ import { Image, ActivityIndicator, Platform, + Alert, } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import { KurdistanColors } from '../theme/colors'; @@ -21,7 +22,7 @@ const showAlert = (title: string, message: string, buttons?: Array<{text: string window.alert(`${title}\n\n${message}`); if (buttons?.[0]?.onPress) buttons[0].onPress(); } else { - showAlert(title, message, buttons); + Alert.alert(title, message, buttons); } }; @@ -109,7 +110,7 @@ const AvatarPickerModal: React.FC = ({ setIsUploading(true); const imageUri = result.assets[0].uri; - if (__DEV__) console.log('[AvatarPicker] Uploading image:', imageUri); + if (__DEV__) console.warn('[AvatarPicker] Uploading image:', imageUri); // Upload to Supabase Storage const uploadedUrl = await uploadImageToSupabase(imageUri); @@ -117,7 +118,7 @@ const AvatarPickerModal: React.FC = ({ setIsUploading(false); if (uploadedUrl) { - if (__DEV__) console.log('[AvatarPicker] Upload successful:', uploadedUrl); + if (__DEV__) console.warn('[AvatarPicker] Upload successful:', uploadedUrl); setUploadedImageUri(uploadedUrl); setSelectedAvatar(null); // Clear emoji selection showAlert('Success', 'Photo uploaded successfully!'); @@ -215,7 +216,7 @@ const AvatarPickerModal: React.FC = ({ return; } - if (__DEV__) console.log('[AvatarPicker] Saving avatar:', avatarToSave); + if (__DEV__) console.warn('[AvatarPicker] Saving avatar:', avatarToSave); setIsSaving(true); @@ -232,7 +233,7 @@ const AvatarPickerModal: React.FC = ({ throw error; } - if (__DEV__) console.log('[AvatarPicker] Avatar saved successfully:', data); + if (__DEV__) console.warn('[AvatarPicker] Avatar saved successfully:', data); showAlert('Success', 'Avatar updated successfully!'); diff --git a/mobile/src/components/Input.tsx b/mobile/src/components/Input.tsx index 9acc846a..6630322a 100644 --- a/mobile/src/components/Input.tsx +++ b/mobile/src/components/Input.tsx @@ -61,7 +61,7 @@ export const Input: React.FC = ({ { setIsFocused(true); props.onFocus?.(e); diff --git a/mobile/src/components/LoadingSkeleton.tsx b/mobile/src/components/LoadingSkeleton.tsx index 9a9b5ad5..6cb98672 100644 --- a/mobile/src/components/LoadingSkeleton.tsx +++ b/mobile/src/components/LoadingSkeleton.tsx @@ -49,7 +49,7 @@ export const Skeleton: React.FC = ({ diff --git a/mobile/src/components/NotificationCenterModal.tsx b/mobile/src/components/NotificationCenterModal.tsx index 7a7e4dca..620c51a1 100644 --- a/mobile/src/components/NotificationCenterModal.tsx +++ b/mobile/src/components/NotificationCenterModal.tsx @@ -8,6 +8,7 @@ import { TouchableOpacity, Alert, } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { KurdistanColors } from '../theme/colors'; import { usePezkuwi } from '../contexts/PezkuwiContext'; import { supabaseHelpers } from '../lib/supabase'; @@ -32,6 +33,7 @@ export const NotificationCenterModal: React.FC = ( visible, onClose, }) => { + const insets = useSafeAreaInsets(); const { selectedAccount } = usePezkuwi(); const [notifications, setNotifications] = useState([]); const [_loading, setLoading] = useState(false); @@ -178,7 +180,7 @@ export const NotificationCenterModal: React.FC = ( onRequestClose={onClose} > - + {/* Header */} diff --git a/mobile/src/components/PezkuwiWebView.tsx b/mobile/src/components/PezkuwiWebView.tsx index 845f2dcc..67bc5006 100644 --- a/mobile/src/components/PezkuwiWebView.tsx +++ b/mobile/src/components/PezkuwiWebView.tsx @@ -14,6 +14,8 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native'; import type { NavigationProp } from '@react-navigation/native'; import { KurdistanColors } from '../theme/colors'; import { usePezkuwi } from '../contexts/PezkuwiContext'; +import { useAuth } from '../contexts/AuthContext'; +import { supabase } from '../lib/supabase'; type RootStackParamList = { Wallet: undefined; @@ -49,6 +51,25 @@ const PezkuwiWebView: React.FC = ({ const navigation = useNavigation>(); const { selectedAccount, getKeyPair, api, isApiReady } = usePezkuwi(); + const { user } = useAuth(); + const [sessionToken, setSessionToken] = useState(null); + const [refreshToken, setRefreshToken] = useState(null); + + // Get Supabase session token for WebView authentication + React.useEffect(() => { + const getSession = async () => { + try { + const { data: { session } } = await supabase.auth.getSession(); + if (session?.access_token) { + setSessionToken(session.access_token); + setRefreshToken(session.refresh_token || null); + } + } catch (error) { + if (__DEV__) console.warn('[WebView] Failed to get session:', error); + } + }; + getSession(); + }, [user]); // JavaScript to inject into the WebView // This creates a bridge between the web app and native app @@ -62,6 +83,36 @@ const PezkuwiWebView: React.FC = ({ ${selectedAccount ? `window.PEZKUWI_ADDRESS = '${selectedAccount.address}';` : ''} ${selectedAccount ? `window.PEZKUWI_ACCOUNT_NAME = '${selectedAccount.meta?.name || 'Mobile Wallet'}';` : ''} + // Inject auth session for automatic login + ${sessionToken ? `window.PEZKUWI_SESSION_TOKEN = '${sessionToken}';` : ''} + ${refreshToken ? `window.PEZKUWI_REFRESH_TOKEN = '${refreshToken}';` : ''} + ${user ? `window.PEZKUWI_USER_ID = '${user.id}';` : ''} + ${user?.email ? `window.PEZKUWI_USER_EMAIL = '${user.email}';` : ''} + + // Auto-authenticate with Supabase if session token exists + if (window.PEZKUWI_SESSION_TOKEN) { + (function autoAuth(attempts = 0) { + if (attempts > 50) { + console.warn('[Mobile] Auto-auth timed out: window.supabase not found'); + return; + } + + if (window.supabase && window.supabase.auth) { + window.supabase.auth.setSession({ + access_token: window.PEZKUWI_SESSION_TOKEN, + refresh_token: window.PEZKUWI_REFRESH_TOKEN || '' + }).then(function(res) { + if (res.error) console.warn('[Mobile] Auto-auth error:', res.error); + else console.log('[Mobile] Auto-authenticated successfully'); + }).catch(function(err) { + console.warn('[Mobile] Auto-auth promise failed:', err); + }); + } else { + setTimeout(function() { autoAuth(attempts + 1); }, 100); + } + })(0); + } + // Override console.log to send to React Native (for debugging) const originalConsoleLog = console.log; console.log = function(...args) { @@ -164,7 +215,7 @@ const PezkuwiWebView: React.FC = ({ const { section, method, args } = payload; if (__DEV__) { - console.log('[WebView] Building transaction:', { section, method, args }); + console.warn('[WebView] Building transaction:', { section, method, args }); } // Get the transaction method from API @@ -187,13 +238,13 @@ const PezkuwiWebView: React.FC = ({ if (result.status.isInBlock) { const hash = result.status.asInBlock?.toString() || ''; if (__DEV__) { - console.log('[WebView] Transaction included in block:', hash); + console.warn('[WebView] Transaction included in block:', hash); } resolve(hash); } else if (result.status.isFinalized) { const hash = result.status.asFinalized?.toString() || ''; if (__DEV__) { - console.log('[WebView] Transaction finalized:', hash); + console.warn('[WebView] Transaction finalized:', hash); } } if (result.dispatchError) { @@ -222,7 +273,7 @@ const PezkuwiWebView: React.FC = ({ case 'CONNECT_WALLET': // Handle wallet connection request from WebView - if (__DEV__) console.log('WebView requested wallet connection'); + if (__DEV__) console.warn('WebView requested wallet connection'); if (selectedAccount) { // Already connected, notify WebView @@ -267,13 +318,13 @@ const PezkuwiWebView: React.FC = ({ case 'CONSOLE_LOG': // Forward console logs from WebView (debug only) if (__DEV__) { - console.log('[WebView]:', message.payload); + console.warn('[WebView]:', message.payload); } break; default: if (__DEV__) { - console.log('Unknown message type:', message.type); + console.warn('Unknown message type:', message.type); } } } catch (parseError) { diff --git a/mobile/src/components/ValidatorSelectionSheet.tsx b/mobile/src/components/ValidatorSelectionSheet.tsx index d37e5143..47b9dab8 100644 --- a/mobile/src/components/ValidatorSelectionSheet.tsx +++ b/mobile/src/components/ValidatorSelectionSheet.tsx @@ -25,7 +25,7 @@ export function ValidatorSelectionSheet({ onClose, onConfirmNominations, }: ValidatorSelectionSheetProps) { - const { api, isApiReady, _selectedAccount } = usePezkuwi(); + const { api, isApiReady } = usePezkuwi(); const [validators, setValidators] = useState([]); const [loading, setLoading] = useState(true); const [processing, _setProcessing] = useState(false); @@ -42,29 +42,28 @@ export function ValidatorSelectionSheet({ // Attempt to fetch from pallet-validator-pool first if (api.query.validatorPool && api.query.validatorPool.validators) { const rawValidators = await api.query.validatorPool.validators(); - // Assuming rawValidators is a list of validator addresses or objects - // This parsing logic will need adjustment based on the exact structure returned - for (const rawValidator of rawValidators.toHuman() as unknown[]) { - // Placeholder: Assume rawValidator is just an address for now + const validatorList = rawValidators.toHuman() as string[]; + for (const rawValidator of validatorList) { chainValidators.push({ - address: rawValidator.toString(), // or rawValidator.address if it's an object - commission: 0.05, // Placeholder: Fetch actual commission - totalStake: '0 HEZ', // Placeholder: Fetch actual stake - selfStake: '0 HEZ', // Placeholder: Fetch actual self stake - nominators: 0, // Placeholder: Fetch actual nominators + address: String(rawValidator), + commission: 0.05, + totalStake: '0 HEZ', + selfStake: '0 HEZ', + nominators: 0, }); } } else { - // Fallback to general staking validators if validatorPool pallet is not found/used - const rawStakingValidators = await api.query.staking.validators() as { keys?: { args: unknown[] }[] }; - for (const validatorAddress of (rawStakingValidators.keys || [])) { - const address = validatorAddress.args[0].toString(); - // Fetch more details about each validator if needed, e.g., commission, total stake - const validatorPrefs = await api.query.staking.validators(address) as { commission: { toNumber: () => number } }; - const commission = validatorPrefs.commission.toNumber() / 10_000_000; // Assuming 10^7 for percentage + // Fallback to session validators + const sessionValidators = await api.query.session.validators(); + const validatorAddresses = sessionValidators.toJSON() as string[]; + + for (const address of validatorAddresses) { + const validatorPrefs = await api.query.staking.validators(address); + const prefsJson = validatorPrefs.toJSON() as { commission?: number } | null; + const commission = prefsJson?.commission + ? Number(prefsJson.commission) / 1_000_000_000 + : 0.05; - // For simplicity, total stake and nominators are placeholders for now - // A more complete implementation would query for detailed exposure chainValidators.push({ address: address, commission: commission, diff --git a/mobile/src/components/wallet/AddTokenModal.tsx b/mobile/src/components/wallet/AddTokenModal.tsx index 0ae0f2a2..9a522022 100644 --- a/mobile/src/components/wallet/AddTokenModal.tsx +++ b/mobile/src/components/wallet/AddTokenModal.tsx @@ -56,9 +56,9 @@ export const AddTokenModal: React.FC = ({ } else { const metadata = metadataOption.toJSON() as { symbol?: string; decimals?: number; name?: string } | null; setTokenMetadata({ - symbol: metadata.symbol || 'UNKNOWN', - decimals: metadata.decimals || 12, - name: metadata.name || 'Unknown Token', + symbol: metadata?.symbol || 'UNKNOWN', + decimals: metadata?.decimals || 12, + name: metadata?.name || 'Unknown Token', }); } } catch { diff --git a/mobile/src/screens/BeCitizenApplyScreen.tsx b/mobile/src/screens/BeCitizenApplyScreen.tsx index 2927db80..dc738a2b 100644 --- a/mobile/src/screens/BeCitizenApplyScreen.tsx +++ b/mobile/src/screens/BeCitizenApplyScreen.tsx @@ -4,7 +4,6 @@ import { Text, TouchableOpacity, StyleSheet, - SafeAreaView, ScrollView, StatusBar, TextInput, @@ -12,6 +11,7 @@ import { ActivityIndicator, Modal, } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { usePezkuwi } from '../contexts/PezkuwiContext'; import { uploadToIPFS, FOUNDER_ADDRESS } from '../../shared/lib/citizenship-workflow'; diff --git a/mobile/src/screens/BeCitizenScreen.tsx b/mobile/src/screens/BeCitizenScreen.tsx index 38325433..6e9f7664 100644 --- a/mobile/src/screens/BeCitizenScreen.tsx +++ b/mobile/src/screens/BeCitizenScreen.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { SafeAreaView, StyleSheet } from 'react-native'; -import { PezkuwiWebView } from '../components'; +import { StyleSheet } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { WebView } from 'react-native-webview'; /** * Be Citizen Screen diff --git a/mobile/src/screens/DashboardScreen.tsx b/mobile/src/screens/DashboardScreen.tsx index 6dac50f3..068dfd8b 100644 --- a/mobile/src/screens/DashboardScreen.tsx +++ b/mobile/src/screens/DashboardScreen.tsx @@ -252,9 +252,9 @@ const DashboardScreen: React.FC = () => { > {isEmoji ? ( - {icon} + {icon as string} ) : ( - + )} {comingSoon && ( @@ -468,7 +468,7 @@ const DashboardScreen: React.FC = () => { {kycStatus === 'NotStarted' && ( navigation.navigate('BeCitizen')} + onPress={() => navigation.navigate('BeCitizenChoice')} > Apply diff --git a/mobile/src/screens/EducationScreen.tsx b/mobile/src/screens/EducationScreen.tsx index 26736f6a..08d3b556 100644 --- a/mobile/src/screens/EducationScreen.tsx +++ b/mobile/src/screens/EducationScreen.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { SafeAreaView, StyleSheet } from 'react-native'; -import { PezkuwiWebView } from '../components'; +import { StyleSheet } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { WebView } from 'react-native-webview'; /** * Education (Perwerde) Screen diff --git a/mobile/src/screens/ForumScreen.tsx b/mobile/src/screens/ForumScreen.tsx index 63209e93..5f15a64f 100644 --- a/mobile/src/screens/ForumScreen.tsx +++ b/mobile/src/screens/ForumScreen.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { SafeAreaView, StyleSheet } from 'react-native'; -import { PezkuwiWebView } from '../components'; +import { StyleSheet } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { WebView } from 'react-native-webview'; /** * Forum Screen diff --git a/mobile/src/screens/GovernanceScreen.tsx b/mobile/src/screens/GovernanceScreen.tsx index 3afe5a5d..998b851f 100644 --- a/mobile/src/screens/GovernanceScreen.tsx +++ b/mobile/src/screens/GovernanceScreen.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { SafeAreaView, StyleSheet } from 'react-native'; -import { PezkuwiWebView } from '../components'; +import { StyleSheet } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { WebView } from 'react-native-webview'; /** * Governance Screen diff --git a/mobile/src/screens/P2PScreen.tsx b/mobile/src/screens/P2PScreen.tsx index c1af39f2..0c2fbc79 100644 --- a/mobile/src/screens/P2PScreen.tsx +++ b/mobile/src/screens/P2PScreen.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { SafeAreaView, StyleSheet } from 'react-native'; -import { PezkuwiWebView } from '../components'; +import { StyleSheet } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { WebView } from 'react-native-webview'; /** * P2P Trading Screen diff --git a/mobile/src/screens/PerwerdeScreen.tsx b/mobile/src/screens/PerwerdeScreen.tsx index d6051147..affaf6e3 100644 --- a/mobile/src/screens/PerwerdeScreen.tsx +++ b/mobile/src/screens/PerwerdeScreen.tsx @@ -61,7 +61,7 @@ type TabType = 'courses' | 'enrolled' | 'completed'; const PerwerdeScreen: React.FC = () => { const _navigation = useNavigation(); - const { selectedAccount, api, isApiReady } = usePezkuwi(); + const { selectedAccount, api, isApiReady, getKeyPair } = usePezkuwi(); const isConnected = !!selectedAccount; // State @@ -199,11 +199,17 @@ const PerwerdeScreen: React.FC = () => { try { const extrinsic = api.tx.perwerde.enroll(courseId); + const keypair = await getKeyPair(selectedAccount.address); + + if (!keypair) { + Alert.alert('Xeletî / Error', 'Could not get signer'); + setEnrolling(false); + return; + } await new Promise((resolve, reject) => { extrinsic.signAndSend( - selectedAccount.address, - { signer: selectedAccount.signer }, + keypair, ({ status, dispatchError }) => { if (dispatchError) { if (dispatchError.isModule) { @@ -767,7 +773,7 @@ const styles = StyleSheet.create({ statusAvailable: { fontSize: 12, fontWeight: '600', - color: KurdistanColors.yer, + color: KurdistanColors.zer, }, statusEnrolled: { fontSize: 12, diff --git a/mobile/src/screens/SettingsScreen.tsx b/mobile/src/screens/SettingsScreen.tsx index 0fb1d7d7..4cba7171 100644 --- a/mobile/src/screens/SettingsScreen.tsx +++ b/mobile/src/screens/SettingsScreen.tsx @@ -4,7 +4,6 @@ import { Text, TouchableOpacity, StyleSheet, - SafeAreaView, ScrollView, StatusBar, Alert, @@ -15,8 +14,10 @@ import { TextInput, Platform } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import AsyncStorage from '@react-native-async-storage/async-storage'; import * as SecureStore from 'expo-secure-store'; +import * as LocalAuthentication from 'expo-local-authentication'; import { Clipboard } from 'react-native'; import { useNavigation, NavigationProp } from '@react-navigation/native'; import type { AlertButton } from 'react-native'; @@ -76,10 +77,10 @@ const AUTO_LOCK_OPTIONS: { value: number; label: string }[] = [ // --- COMPONENTS (Internal for simplicity) --- const SectionHeader = ({ title }: { title: string }) => { - const { colors } = useTheme(); + const { colors, fontScale } = useTheme(); return ( - {title} + {title} ); }; @@ -101,7 +102,7 @@ const SettingItem = ({ textColor?: string; testID?: string; }) => { - const { colors } = useTheme(); + const { colors, fontScale } = useTheme(); return ( {icon} - {title} - {subtitle && {subtitle}} + {title} + {subtitle && {subtitle}} {showArrow && } @@ -137,15 +138,15 @@ const SettingToggle = ({ loading?: boolean; testID?: string; }) => { - const { colors } = useTheme(); + const { colors, fontScale } = useTheme(); return ( {icon} - {title} - {subtitle && {subtitle}} + {title} + {subtitle && {subtitle}} {loading ? ( @@ -187,6 +188,8 @@ const SettingsScreen: React.FC = () => { const [showFontSizeModal, setShowFontSizeModal] = useState(false); const [showAutoLockModal, setShowAutoLockModal] = useState(false); const [showBackupModal, setShowBackupModal] = useState(false); + const [showTermsModal, setShowTermsModal] = useState(false); + const [showPrivacyModal, setShowPrivacyModal] = useState(false); const [backupMnemonic, setBackupMnemonic] = useState(''); const [editName, setEditName] = useState(''); const [editBio, setEditBio] = useState(''); @@ -209,7 +212,7 @@ const SettingsScreen: React.FC = () => { setEditBio(data.bio || ''); } } catch (_err) { - console.log('Error fetching profile:', _err); + if (__DEV__) console.warn('Error fetching profile:', _err); } finally { setLoadingProfile(false); } @@ -324,34 +327,68 @@ const SettingsScreen: React.FC = () => { return option ? option.label : '5 minutes'; }; - // 8. Wallet Backup Handler + // 8. Wallet Backup Handler - with security confirmation const handleWalletBackup = async () => { if (!selectedAccount) { showAlert('No Wallet', 'Please create or import a wallet first.'); return; } - try { - // Retrieve mnemonic from secure storage - const seedKey = `pezkuwi_seed_${selectedAccount.address}`; - let storedMnemonic: string | null = null; + // Security warning before showing recovery phrase + showAlert( + '⚠️ Security Warning', + 'Your recovery phrase is the only way to restore your wallet. NEVER share it with anyone.\n\n• Anyone with this phrase can steal your funds\n• Pezkuwi will NEVER ask for your phrase\n• Write it down and store it safely\n\nDo you want to continue?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'I Understand', + style: 'destructive', + onPress: async () => { + // If biometric is enabled, require authentication + if (isBiometricEnabled && Platform.OS !== 'web') { + try { + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Authenticate to view recovery phrase', + cancelLabel: 'Cancel', + disableDeviceFallback: false, + }); - if (Platform.OS === 'web') { - storedMnemonic = await AsyncStorage.getItem(seedKey); - } else { - storedMnemonic = await SecureStore.getItemAsync(seedKey); - } + if (!result.success) { + showAlert('Authentication Failed', 'Could not verify your identity.'); + return; + } + } catch (error) { + console.error('Biometric auth error:', error); + showAlert('Error', 'Authentication failed.'); + return; + } + } - if (storedMnemonic) { - setBackupMnemonic(storedMnemonic); - setShowBackupModal(true); - } else { - showAlert('No Backup', 'Recovery phrase not found. It may have been imported from another device.'); - } - } catch (error) { - console.error('Error retrieving mnemonic:', error); - showAlert('Error', 'Failed to retrieve recovery phrase.'); - } + // Retrieve mnemonic from secure storage + try { + const seedKey = `pezkuwi_seed_${selectedAccount.address}`; + let storedMnemonic: string | null = null; + + if (Platform.OS === 'web') { + storedMnemonic = await AsyncStorage.getItem(seedKey); + } else { + storedMnemonic = await SecureStore.getItemAsync(seedKey); + } + + if (storedMnemonic) { + setBackupMnemonic(storedMnemonic); + setShowBackupModal(true); + } else { + showAlert('No Backup', 'Recovery phrase not found. It may have been imported from another device.'); + } + } catch (error) { + console.error('Error retrieving mnemonic:', error); + showAlert('Error', 'Failed to retrieve recovery phrase.'); + } + } + } + ] + ); }; return ( @@ -378,10 +415,10 @@ const SettingsScreen: React.FC = () => { testID="edit-profile-button" /> navigation.navigate('Wallet')} + icon="👛" + title="My Wallet" + subtitle={selectedAccount ? `View balance & transactions` : 'Set up your wallet'} + onPress={() => selectedAccount ? navigation.navigate('Wallet') : navigation.navigate('WalletSetup')} testID="wallet-management-button" /> @@ -470,13 +507,13 @@ const SettingsScreen: React.FC = () => { showAlert('Terms', 'Terms of service content...')} + onPress={() => setShowTermsModal(true)} testID="terms-of-service-button" /> showAlert('Privacy', 'Privacy policy content...')} + onPress={() => setShowPrivacyModal(true)} testID="privacy-policy-button" /> { + {/* TERMS OF SERVICE MODAL */} + + + + + Terms of Service + setShowTermsModal(false)}> + Done + + + + Pezkuwi Terms of Service + Effective Date: {new Date().getFullYear()} + + 1. Acceptance of Terms + + By accessing or using the Pezkuwi application ("App"), you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use the App. + + + 2. Description of Service + + Pezkuwi is a decentralized application that provides access to the Digital Kurdistan blockchain network. The App enables users to:{'\n'} + • Create and manage blockchain wallets{'\n'} + • Send and receive PEZ and HEZ tokens{'\n'} + • Participate in governance and voting{'\n'} + • Access decentralized services within the ecosystem + + + 3. User Responsibilities + + You are solely responsible for:{'\n'} + • Maintaining the security of your wallet and recovery phrase{'\n'} + • All activities conducted through your account{'\n'} + • Ensuring compliance with local laws and regulations{'\n'} + • Any transactions you authorize on the blockchain + + + 4. Wallet Security + + Your wallet is secured by a recovery phrase (seed phrase). This phrase is the ONLY way to restore access to your wallet. Pezkuwi does not store your recovery phrase and cannot recover it if lost. You must:{'\n'} + • Never share your recovery phrase with anyone{'\n'} + • Store your recovery phrase securely offline{'\n'} + • Understand that lost phrases cannot be recovered + + + 5. Disclaimer of Warranties + + The App is provided "as is" without warranties of any kind. We do not guarantee uninterrupted access, error-free operation, or specific outcomes from using the App. + + + 6. Limitation of Liability + + Pezkuwi and its affiliates shall not be liable for any direct, indirect, incidental, consequential, or punitive damages arising from your use of the App, including but not limited to loss of funds or data. + + + 7. Modifications + + We reserve the right to modify these Terms at any time. Continued use of the App after changes constitutes acceptance of the modified Terms. + + + 8. Contact + + For questions about these Terms, contact us at:{'\n'} + support@pezkuwichain.io + + + + + + + + {/* PRIVACY POLICY MODAL */} + + + + + Privacy Policy + setShowPrivacyModal(false)}> + Done + + + + Pezkuwi Privacy Policy + Effective Date: {new Date().getFullYear()} + + 1. Introduction + + Pezkuwi ("we", "our", "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, and safeguard your information when you use our application. + + + 2. Information We Collect + + Account Information: Email address (if provided for authentication), username, and profile information you choose to provide.{'\n\n'} + Blockchain Data: Your public wallet address and transaction history (which is publicly visible on the blockchain).{'\n\n'} + Device Information: Device type, operating system, and app version for troubleshooting and improvement purposes.{'\n\n'} + We DO NOT collect: Your recovery phrase, private keys, or biometric data (stored only on your device). + + + 3. How We Use Information + + We use collected information to:{'\n'} + • Provide and maintain our services{'\n'} + • Process your transactions on the blockchain{'\n'} + • Send important notifications about your account{'\n'} + • Improve our application and user experience{'\n'} + • Comply with legal obligations + + + 4. Data Storage and Security + + • Your recovery phrase and private keys are stored ONLY on your device using secure storage mechanisms{'\n'} + • We employ industry-standard security measures to protect your data{'\n'} + • Blockchain transactions are permanently recorded on the public ledger + + + 5. Data Sharing + + We do not sell your personal information. We may share data with:{'\n'} + • Service providers who assist in operating our services{'\n'} + • Legal authorities when required by law{'\n'} + • Blockchain networks for transaction processing (public data) + + + 6. Your Rights + + You have the right to:{'\n'} + • Access your personal data{'\n'} + • Request correction of inaccurate data{'\n'} + • Request deletion of your account{'\n'} + • Opt-out of non-essential communications{'\n\n'} + Note: Blockchain data cannot be deleted due to its immutable nature. + + + 7. Children's Privacy + + Our services are not intended for users under 18 years of age. We do not knowingly collect information from children. + + + 8. Updates to Policy + + We may update this Privacy Policy periodically. We will notify you of significant changes through the App or via email. + + + 9. Contact Us + + For privacy-related inquiries:{'\n'} + Email: privacy@pezkuwichain.io{'\n'} + Support: support@pezkuwichain.io + + + + + + + ); }; @@ -937,6 +1129,26 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '600', }, + // Legal Modal Styles + legalTitle: { + fontSize: 22, + fontWeight: 'bold', + marginBottom: 8, + }, + legalDate: { + fontSize: 13, + marginBottom: 24, + }, + legalSectionTitle: { + fontSize: 16, + fontWeight: '700', + marginTop: 20, + marginBottom: 10, + }, + legalText: { + fontSize: 14, + lineHeight: 22, + }, }); export default SettingsScreen; \ No newline at end of file diff --git a/mobile/src/screens/WalletScreen.tsx b/mobile/src/screens/WalletScreen.tsx index 1df413b4..c8df6f04 100644 --- a/mobile/src/screens/WalletScreen.tsx +++ b/mobile/src/screens/WalletScreen.tsx @@ -104,6 +104,7 @@ const WalletScreen: React.FC = () => { accounts, selectedAccount, setSelectedAccount, + connectWallet, createWallet, deleteWallet, getKeyPair, @@ -114,8 +115,8 @@ const WalletScreen: React.FC = () => { const [selectedToken, setSelectedToken] = useState(null); const [sendModalVisible, setSendModalVisible] = useState(false); const [receiveModalVisible, setReceiveModalVisible] = useState(false); - const [_createWalletModalVisible, _setCreateWalletModalVisible] = useState(false); - const [_importWalletModalVisible, _setImportWalletModalVisible] = useState(false); + const [createWalletModalVisible, setCreateWalletModalVisible] = useState(false); + const [importWalletModalVisible, setImportWalletModalVisible] = useState(false); const [backupModalVisible, setBackupModalVisible] = useState(false); const [networkSelectorVisible, setNetworkSelectorVisible] = useState(false); const [walletSelectorVisible, setWalletSelectorVisible] = useState(false); @@ -127,16 +128,16 @@ const WalletScreen: React.FC = () => { const [hiddenTokens, setHiddenTokens] = useState([]); const [recipientAddress, setRecipientAddress] = useState(''); const [sendAmount, setSendAmount] = useState(''); - const [walletName, _setWalletName] = useState(''); + const [walletName, setWalletName] = useState(''); const [importMnemonic, setImportMnemonic] = useState(''); - const [_importWalletName, _setImportWalletName] = useState(''); + const [importWalletName, setImportWalletName] = useState(''); const [userMnemonic, setUserMnemonic] = useState(''); const [isSending, setIsSending] = useState(false); const [isLoadingBalances, setIsLoadingBalances] = useState(false); // Transaction History State (TODO: implement transaction history display) - const [_transactions, _setTransactions] = useState([]); - const [_isLoadingHistory, _setIsLoadingHistory] = useState(false); + const [transactions, setTransactions] = useState([]); + const [isLoadingHistory, setIsLoadingHistory] = useState(false); const [balances, setBalances] = useState<{ [key: string]: string }>({ HEZ: '0.00', @@ -570,7 +571,7 @@ const WalletScreen: React.FC = () => { throw new Error('Unknown token type'); } - await tx.signAndSend(keypair, ({ status, _events }) => { + await tx.signAndSend(keypair, ({ status, events }) => { if (status.isInBlock) { if (__DEV__) console.warn('[Wallet] Transaction in block:', status.asInBlock.toHex()); } @@ -598,7 +599,7 @@ const WalletScreen: React.FC = () => { const _handleCreateWallet = async () => { try { - const { _address, mnemonic } = await createWallet(walletName); + const { address, mnemonic } = await createWallet(walletName); setUserMnemonic(mnemonic); // Save for backup setCreateWalletModalVisible(false); showAlert('Wallet Created', `Save this mnemonic:\n${mnemonic}`, [{ text: 'OK', onPress: () => connectWallet() }]); @@ -685,7 +686,7 @@ const WalletScreen: React.FC = () => { // Redirect to WalletSetupScreen if no wallet exists useEffect(() => { if (!selectedAccount && accounts.length === 0) { - navigation.replace('WalletSetup'); + (navigation as any).replace('WalletSetup'); } }, [selectedAccount, accounts, navigation]); @@ -1491,11 +1492,6 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, - mainTokenLogo: { - width: 56, - height: 56, - borderRadius: 28, - }, mainTokenSymbol: { fontSize: 18, fontWeight: 'bold', diff --git a/mobile/src/screens/WalletSetupScreen.tsx b/mobile/src/screens/WalletSetupScreen.tsx index 596c96bb..f19ee977 100644 --- a/mobile/src/screens/WalletSetupScreen.tsx +++ b/mobile/src/screens/WalletSetupScreen.tsx @@ -181,9 +181,10 @@ const WalletSetupScreen: React.FC = () => { } }; - // Finish setup and go to wallet - const handleFinish = () => { - navigation.replace('Wallet'); + const handleSuccess = () => { + // Navigate to main wallet screen + // Using replace to prevent going back to setup + (navigation as any).replace('Wallet'); }; // Go back to previous step (TODO: add back button UI) @@ -459,7 +460,7 @@ const WalletSetupScreen: React.FC = () => { Go to Wallet diff --git a/mobile/src/screens/governance/DelegationScreen.tsx b/mobile/src/screens/governance/DelegationScreen.tsx index b478c721..5ea107cc 100644 --- a/mobile/src/screens/governance/DelegationScreen.tsx +++ b/mobile/src/screens/governance/DelegationScreen.tsx @@ -75,7 +75,7 @@ const DelegationScreen: React.FC = () => { const votingEntries = await api.query.democracy.voting.entries(); const delegatesMap = new Map(); - votingEntries.forEach(([key, value]: [{ args: [{ toString: () => string }] }, { isDelegating: boolean; asDelegating: { target: { toString: () => string }; balance: { toString: () => string } } }]) => { + votingEntries.forEach(([key, value]: any) => { const _voter = key.args[0].toString(); const voting = value; diff --git a/mobile/src/screens/governance/ProposalsScreen.tsx b/mobile/src/screens/governance/ProposalsScreen.tsx index 22819117..18be11fa 100644 --- a/mobile/src/screens/governance/ProposalsScreen.tsx +++ b/mobile/src/screens/governance/ProposalsScreen.tsx @@ -51,7 +51,7 @@ const ProposalsScreen: React.FC = () => { // Fetch democracy referenda if (api.query.democracy?.referendumInfoOf) { const referendaData = await api.query.democracy.referendumInfoOf.entries(); - const parsedProposals: Proposal[] = referendaData.map(([key, value]: [{ args: [{ toNumber(): number }] }, { unwrap(): { isOngoing?: boolean; asOngoing?: { tally?: { ayes?: { toString(): string }; nays?: { toString(): string } }; proposalHash?: { toString(): string }; end?: { toNumber(): number } } } }]) => { + const parsedProposals: Proposal[] = referendaData.map(([key, value]: any) => { const index = key.args[0].toNumber(); const info = value.unwrap(); diff --git a/mobile/src/screens/governance/TreasuryScreen.tsx b/mobile/src/screens/governance/TreasuryScreen.tsx index 35c212d1..726900c7 100644 --- a/mobile/src/screens/governance/TreasuryScreen.tsx +++ b/mobile/src/screens/governance/TreasuryScreen.tsx @@ -57,7 +57,7 @@ const TreasuryScreen: React.FC = () => { // Fetch treasury proposals if (api.query.treasury?.proposals) { const proposalsData = await api.query.treasury.proposals.entries(); - const parsedProposals: TreasuryProposal[] = proposalsData.map(([key, value]: [{ args: [{ toNumber(): number }] }, { unwrap(): { beneficiary: { toString(): string }; value: { toString(): string }; proposer: { toString(): string }; bond: { toString(): string } } }]) => { + const parsedProposals: TreasuryProposal[] = proposalsData.map(([key, value]: any) => { const proposalIndex = key.args[0].toNumber(); const proposal = value.unwrap(); diff --git a/mobile/src/services/TokenService.ts b/mobile/src/services/TokenService.ts index bcd76e98..c745c3ff 100644 --- a/mobile/src/services/TokenService.ts +++ b/mobile/src/services/TokenService.ts @@ -25,7 +25,7 @@ export interface TokenInfo { usdValue: string; priceUsd: number; change24h: number; - logo: string | null; + logo: ImageSourcePropType | null; isNative: boolean; isFrozen: boolean; } diff --git a/mobile/tsconfig.json b/mobile/tsconfig.json index bc3e5d50..ed42ded5 100644 --- a/mobile/tsconfig.json +++ b/mobile/tsconfig.json @@ -13,5 +13,6 @@ }, "typeRoots": ["./src/types", "./node_modules/@types"] }, - "include": ["src/**/*.ts", "src/**/*.tsx", "shared/**/*.ts", "App.tsx", "index.ts", "__mocks__/**/*.ts", "__mocks__/**/*.tsx"] + "include": ["src/**/*.ts", "src/**/*.tsx", "shared/**/*.ts", "App.tsx", "index.ts"], + "exclude": ["**/__tests__/**/*", "**/__mocks__/**/*", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"] }