chore: Fix 430+ TS errors, Safe Area issues, WebView SSO, and mock Alice for testing

This commit is contained in:
2026-01-19 03:06:22 +03:00
parent e0bf620f56
commit c7eab6d78a
27 changed files with 411 additions and 137 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{
"expo": {
"name": "Pezkuwi",
"slug": "pezkuwi",
"name": "PezkuwiApp",
"slug": "pezkuwiapp",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
+6 -6
View File
@@ -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');
+2 -2
View File
@@ -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
+14 -14
View File
@@ -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"
},
+6 -5
View File
@@ -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<AvatarPickerModalProps> = ({
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<AvatarPickerModalProps> = ({
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<AvatarPickerModalProps> = ({
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<AvatarPickerModalProps> = ({
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!');
+1 -1
View File
@@ -61,7 +61,7 @@ export const Input: React.FC<InputProps> = ({
<TextInput
{...props}
editable={props.editable !== undefined ? props.editable : !disabled}
style={[styles.input, leftIcon && styles.inputWithLeftIcon, style]}
style={[styles.input, !!leftIcon && styles.inputWithLeftIcon, style]}
onFocus={(e) => {
setIsFocused(true);
props.onFocus?.(e);
+1 -1
View File
@@ -49,7 +49,7 @@ export const Skeleton: React.FC<SkeletonProps> = ({
<Animated.View
style={[
styles.skeleton,
{ width, height, borderRadius, opacity },
{ width: width as any, height, borderRadius, opacity },
style,
]}
/>
@@ -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<NotificationCenterModalProps> = (
visible,
onClose,
}) => {
const insets = useSafeAreaInsets();
const { selectedAccount } = usePezkuwi();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [_loading, setLoading] = useState(false);
@@ -178,7 +180,7 @@ export const NotificationCenterModal: React.FC<NotificationCenterModalProps> = (
onRequestClose={onClose}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={[styles.modalContent, { paddingBottom: Math.max(insets.bottom, 20) }]}>
{/* Header */}
<View style={styles.header}>
<View>
+57 -6
View File
@@ -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<PezkuwiWebViewProps> = ({
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { selectedAccount, getKeyPair, api, isApiReady } = usePezkuwi();
const { user } = useAuth();
const [sessionToken, setSessionToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(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<PezkuwiWebViewProps> = ({
${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<PezkuwiWebViewProps> = ({
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<PezkuwiWebViewProps> = ({
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<PezkuwiWebViewProps> = ({
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<PezkuwiWebViewProps> = ({
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) {
@@ -25,7 +25,7 @@ export function ValidatorSelectionSheet({
onClose,
onConfirmNominations,
}: ValidatorSelectionSheetProps) {
const { api, isApiReady, _selectedAccount } = usePezkuwi();
const { api, isApiReady } = usePezkuwi();
const [validators, setValidators] = useState<Validator[]>([]);
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,
@@ -56,9 +56,9 @@ export const AddTokenModal: React.FC<AddTokenModalProps> = ({
} 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 {
+1 -1
View File
@@ -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';
+3 -2
View File
@@ -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
+3 -3
View File
@@ -252,9 +252,9 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
>
<View style={[styles.appIconBox, comingSoon && styles.appIconDisabled]}>
{isEmoji ? (
<Text style={styles.emojiIcon}>{icon}</Text>
<Text style={styles.emojiIcon}>{icon as string}</Text>
) : (
<Image source={icon} style={styles.imageIcon} resizeMode="cover" />
<Image source={icon as ImageSourcePropType} style={styles.imageIcon} resizeMode="cover" />
)}
{comingSoon && (
<View style={styles.lockBadge}>
@@ -468,7 +468,7 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
{kycStatus === 'NotStarted' && (
<TouchableOpacity
style={styles.kycButton}
onPress={() => navigation.navigate('BeCitizen')}
onPress={() => navigation.navigate('BeCitizenChoice')}
>
<Text style={styles.kycButtonText}>Apply</Text>
</TouchableOpacity>
+3 -2
View File
@@ -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
+3 -2
View File
@@ -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
+3 -2
View File
@@ -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
+3 -2
View File
@@ -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
+10 -4
View File
@@ -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<void>((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,
+248 -36
View File
@@ -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 (
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: colors.textSecondary }]}>{title}</Text>
<Text style={[styles.sectionTitle, { color: colors.textSecondary, fontSize: 12 * fontScale }]}>{title}</Text>
</View>
);
};
@@ -101,7 +102,7 @@ const SettingItem = ({
textColor?: string;
testID?: string;
}) => {
const { colors } = useTheme();
const { colors, fontScale } = useTheme();
return (
<TouchableOpacity
style={[styles.settingItem, { borderBottomColor: colors.border }]}
@@ -112,8 +113,8 @@ const SettingItem = ({
<Text style={styles.settingIconText}>{icon}</Text>
</View>
<View style={styles.settingContent}>
<Text style={[styles.settingTitle, { color: textColor || colors.text }]}>{title}</Text>
{subtitle && <Text style={[styles.settingSubtitle, { color: colors.textSecondary }]}>{subtitle}</Text>}
<Text style={[styles.settingTitle, { color: textColor || colors.text, fontSize: 16 * fontScale }]}>{title}</Text>
{subtitle && <Text style={[styles.settingSubtitle, { color: colors.textSecondary, fontSize: 13 * fontScale }]}>{subtitle}</Text>}
</View>
{showArrow && <Text style={[styles.arrow, { color: colors.textSecondary }]}></Text>}
</TouchableOpacity>
@@ -137,15 +138,15 @@ const SettingToggle = ({
loading?: boolean;
testID?: string;
}) => {
const { colors } = useTheme();
const { colors, fontScale } = useTheme();
return (
<View style={[styles.settingItem, { borderBottomColor: colors.border }]} testID={testID}>
<View style={[styles.settingIcon, { backgroundColor: colors.background }]}>
<Text style={styles.settingIconText}>{icon}</Text>
</View>
<View style={styles.settingContent}>
<Text style={[styles.settingTitle, { color: colors.text }]}>{title}</Text>
{subtitle && <Text style={[styles.settingSubtitle, { color: colors.textSecondary }]}>{subtitle}</Text>}
<Text style={[styles.settingTitle, { color: colors.text, fontSize: 16 * fontScale }]}>{title}</Text>
{subtitle && <Text style={[styles.settingSubtitle, { color: colors.textSecondary, fontSize: 13 * fontScale }]}>{subtitle}</Text>}
</View>
{loading ? (
<ActivityIndicator size="small" color={KurdistanColors.kesk} />
@@ -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"
/>
<SettingItem
icon="💳"
title="Wallet Management"
subtitle="Manage your connected keys"
onPress={() => 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"
/>
</View>
@@ -470,13 +507,13 @@ const SettingsScreen: React.FC = () => {
<SettingItem
icon="📄"
title="Terms of Service"
onPress={() => showAlert('Terms', 'Terms of service content...')}
onPress={() => setShowTermsModal(true)}
testID="terms-of-service-button"
/>
<SettingItem
icon="🔒"
title="Privacy Policy"
onPress={() => showAlert('Privacy', 'Privacy policy content...')}
onPress={() => setShowPrivacyModal(true)}
testID="privacy-policy-button"
/>
<SettingItem
@@ -733,6 +770,161 @@ const SettingsScreen: React.FC = () => {
</View>
</Modal>
{/* TERMS OF SERVICE MODAL */}
<Modal visible={showTermsModal} animationType="slide" testID="terms-modal">
<SafeAreaView style={[styles.fullModal, { backgroundColor: colors.background }]}>
<View style={[styles.header, { backgroundColor: colors.surface, borderBottomColor: colors.border }]}>
<View style={{ width: 60 }} />
<Text style={[styles.headerTitle, { color: colors.text }]}>Terms of Service</Text>
<TouchableOpacity onPress={() => setShowTermsModal(false)}>
<Text style={{ fontSize: 16, color: KurdistanColors.kesk, fontWeight: '600' }}>Done</Text>
</TouchableOpacity>
</View>
<ScrollView style={{ flex: 1, padding: 20 }}>
<Text style={[styles.legalTitle, { color: colors.text }]}>Pezkuwi Terms of Service</Text>
<Text style={[styles.legalDate, { color: colors.textSecondary }]}>Effective Date: {new Date().getFullYear()}</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>1. Acceptance of Terms</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
By accessing or using the Pezkuwi application (&quot;App&quot;), you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use the App.
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>2. Description of Service</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
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
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>3. User Responsibilities</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
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
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>4. Wallet Security</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
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
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>5. Disclaimer of Warranties</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
The App is provided &quot;as is&quot; without warranties of any kind. We do not guarantee uninterrupted access, error-free operation, or specific outcomes from using the App.
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>6. Limitation of Liability</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
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.
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>7. Modifications</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
We reserve the right to modify these Terms at any time. Continued use of the App after changes constitutes acceptance of the modified Terms.
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>8. Contact</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
For questions about these Terms, contact us at:{'\n'}
support@pezkuwichain.io
</Text>
<View style={{ height: 40 }} />
</ScrollView>
</SafeAreaView>
</Modal>
{/* PRIVACY POLICY MODAL */}
<Modal visible={showPrivacyModal} animationType="slide" testID="privacy-modal">
<SafeAreaView style={[styles.fullModal, { backgroundColor: colors.background }]}>
<View style={[styles.header, { backgroundColor: colors.surface, borderBottomColor: colors.border }]}>
<View style={{ width: 60 }} />
<Text style={[styles.headerTitle, { color: colors.text }]}>Privacy Policy</Text>
<TouchableOpacity onPress={() => setShowPrivacyModal(false)}>
<Text style={{ fontSize: 16, color: KurdistanColors.kesk, fontWeight: '600' }}>Done</Text>
</TouchableOpacity>
</View>
<ScrollView style={{ flex: 1, padding: 20 }}>
<Text style={[styles.legalTitle, { color: colors.text }]}>Pezkuwi Privacy Policy</Text>
<Text style={[styles.legalDate, { color: colors.textSecondary }]}>Effective Date: {new Date().getFullYear()}</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>1. Introduction</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
Pezkuwi (&quot;we&quot;, &quot;our&quot;, &quot;us&quot;) is committed to protecting your privacy. This Privacy Policy explains how we collect, use, and safeguard your information when you use our application.
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>2. Information We Collect</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
<Text style={{ fontWeight: '600' }}>Account Information:</Text> Email address (if provided for authentication), username, and profile information you choose to provide.{'\n\n'}
<Text style={{ fontWeight: '600' }}>Blockchain Data:</Text> Your public wallet address and transaction history (which is publicly visible on the blockchain).{'\n\n'}
<Text style={{ fontWeight: '600' }}>Device Information:</Text> Device type, operating system, and app version for troubleshooting and improvement purposes.{'\n\n'}
<Text style={{ fontWeight: '600' }}>We DO NOT collect:</Text> Your recovery phrase, private keys, or biometric data (stored only on your device).
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>3. How We Use Information</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
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
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>4. Data Storage and Security</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
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
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>5. Data Sharing</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
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)
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>6. Your Rights</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
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.
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>7. Children&apos;s Privacy</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
Our services are not intended for users under 18 years of age. We do not knowingly collect information from children.
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>8. Updates to Policy</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
We may update this Privacy Policy periodically. We will notify you of significant changes through the App or via email.
</Text>
<Text style={[styles.legalSectionTitle, { color: colors.text }]}>9. Contact Us</Text>
<Text style={[styles.legalText, { color: colors.textSecondary }]}>
For privacy-related inquiries:{'\n'}
Email: privacy@pezkuwichain.io{'\n'}
Support: support@pezkuwichain.io
</Text>
<View style={{ height: 40 }} />
</ScrollView>
</SafeAreaView>
</Modal>
</SafeAreaView>
);
};
@@ -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;
+10 -14
View File
@@ -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<Token | null>(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<string[]>([]);
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<string>('');
const [isSending, setIsSending] = useState(false);
const [isLoadingBalances, setIsLoadingBalances] = useState(false);
// Transaction History State (TODO: implement transaction history display)
const [_transactions, _setTransactions] = useState<Transaction[]>([]);
const [_isLoadingHistory, _setIsLoadingHistory] = useState(false);
const [transactions, setTransactions] = useState<Transaction[]>([]);
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',
+5 -4
View File
@@ -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 = () => {
<TouchableOpacity
style={styles.primaryButton}
onPress={handleFinish}
onPress={handleSuccess}
testID="wallet-setup-done-button"
>
<Text style={styles.primaryButtonText}>Go to Wallet</Text>
@@ -75,7 +75,7 @@ const DelegationScreen: React.FC = () => {
const votingEntries = await api.query.democracy.voting.entries();
const delegatesMap = new Map<string, { delegated: bigint; count: number }>();
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;
@@ -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();
@@ -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();
+1 -1
View File
@@ -25,7 +25,7 @@ export interface TokenInfo {
usdValue: string;
priceUsd: number;
change24h: number;
logo: string | null;
logo: ImageSourcePropType | null;
isNative: boolean;
isFrozen: boolean;
}
+2 -1
View File
@@ -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"]
}