diff --git a/mobile/App.tsx b/mobile/App.tsx
index e84e2965..e61be44e 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -1,70 +1,25 @@
-import React, { useEffect, useState } from 'react';
-import { View, ActivityIndicator, StyleSheet } from 'react-native';
+import React from 'react';
import { StatusBar } from 'expo-status-bar';
-import { initializeI18n } from './src/i18n';
import { ErrorBoundary } from './src/components/ErrorBoundary';
-import { LanguageProvider } from './src/contexts/LanguageContext';
import { AuthProvider } from './src/contexts/AuthContext';
import { PezkuwiProvider } from './src/contexts/PezkuwiContext';
import { BiometricAuthProvider } from './src/contexts/BiometricAuthContext';
import { ThemeProvider } from './src/contexts/ThemeContext';
import AppNavigator from './src/navigation/AppNavigator';
-import { KurdistanColors } from './src/theme/colors';
export default function App() {
- const [isI18nInitialized, setIsI18nInitialized] = useState(false);
-
- useEffect(() => {
- // Initialize i18n on app start
- const initApp = async () => {
- try {
- console.log('🚀 App starting...');
- console.log('🔧 Initializing i18n...');
- await initializeI18n();
- console.log('✅ i18n initialized');
- setIsI18nInitialized(true);
- } catch (error) {
- console.error('❌ Failed to initialize i18n:', error);
- // Fallback: Still show app but with default language
- setIsI18nInitialized(true);
- }
- };
-
- initApp();
- }, []);
-
- if (!isI18nInitialized) {
- return (
-
-
-
- );
- }
-
return (
-
-
-
-
-
-
+
+
+
+
);
}
-
-const styles = StyleSheet.create({
- loadingContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: KurdistanColors.spi,
- },
-});
-
diff --git a/mobile/PHASE_1_COMPLETE.md b/mobile/PHASE_1_COMPLETE.md
deleted file mode 100644
index 7713fd4f..00000000
--- a/mobile/PHASE_1_COMPLETE.md
+++ /dev/null
@@ -1,408 +0,0 @@
-# ✅ PHASE 1 COMPLETE - Settings Screen Full Implementation
-
-**Date:** 2026-01-14
-**Duration:** ~3 hours
-**Status:** COMPLETE
-
----
-
-## Objective
-
-Make ALL features in Settings screen fully functional - no "Coming Soon" alerts.
-
----
-
-## Changes Made
-
-### 1. Dark Mode ✅
-
-**Files:**
-- `/home/mamostehp/pwap/shared/theme/colors.ts` - Added LightColors & DarkColors
-- `/home/mamostehp/pwap/mobile/src/contexts/ThemeContext.tsx` - Added colors export
-- `/home/mamostehp/pwap/mobile/src/screens/SettingsScreen.tsx` - Connected theme
-
-**Features:**
-- Toggle switches between light/dark theme
-- Theme persists in AsyncStorage (`@pezkuwi/theme`)
-- All screens use dynamic colors from `useTheme().colors`
-- StatusBar adapts to theme (light-content / dark-content)
-
-**Colors:**
-```typescript
-LightColors: {
- background: '#F5F5F5',
- surface: '#FFFFFF',
- text: '#000000',
- textSecondary: '#666666',
- border: '#E0E0E0',
-}
-
-DarkColors: {
- background: '#121212',
- surface: '#1E1E1E',
- text: '#FFFFFF',
- textSecondary: '#B0B0B0',
- border: '#333333',
-}
-```
-
----
-
-### 2. Font Size ✅
-
-**Files:**
-- `/home/mamostehp/pwap/mobile/src/contexts/ThemeContext.tsx` - Added fontSize state
-
-**Features:**
-- 3 sizes: Small (87.5%), Medium (100%), Large (112.5%)
-- Persists in AsyncStorage (`@pezkuwi/font_size`)
-- Exposes `fontScale` multiplier for responsive text
-- Alert dialog for selection
-
-**Usage:**
-```typescript
-const { fontSize, setFontSize, fontScale } = useTheme();
-// fontScale: 0.875 | 1 | 1.125
-```
-
----
-
-### 3. Biometric Authentication ✅
-
-**Files:**
-- `/home/mamostehp/pwap/mobile/src/screens/SettingsScreen.tsx` - Connected BiometricAuthContext
-
-**Features:**
-- Fingerprint / Face ID support via `expo-local-authentication`
-- Checks hardware availability
-- Verifies enrollment before enabling
-- Displays biometric type in subtitle (fingerprint/facial/iris)
-- Full context already existed in BiometricAuthContext.tsx
-
-**Flow:**
-1. User toggles ON → Check if biometric available → Prompt for authentication
-2. If success → Save to AsyncStorage → Show "Enabled (fingerprint)"
-3. User toggles OFF → Disable → Show "Disabled"
-
----
-
-### 4. Change Password ✅
-
-**Files:**
-- `/home/mamostehp/pwap/mobile/src/contexts/AuthContext.tsx` - Updated changePassword signature
-- `/home/mamostehp/pwap/mobile/src/components/ChangePasswordModal.tsx` - NEW
-
-**Features:**
-- **Current Password verification** - Re-authenticates with Supabase before changing
-- **New Password** - Minimum 6 characters
-- **Confirm Password** - Must match new password
-- **Forgot Password link** - Sends reset email via Supabase
-- Full validation with error messages
-
-**Implementation:**
-```typescript
-// AuthContext
-changePassword(newPassword, currentPassword) {
- // 1. Verify current password by sign in
- // 2. If correct, update to new password
- // 3. Return error or success
-}
-
-resetPassword(email) {
- // Send password reset email
-}
-```
-
----
-
-### 5. Email Notifications ✅
-
-**Files:**
-- `/home/mamostehp/pwap/mobile/src/components/EmailNotificationsModal.tsx` - NEW
-
-**Features:**
-- 4 categories with toggle switches:
- - 💸 Transaction Updates
- - 🗳️ Governance Alerts
- - 🔒 Security Alerts
- - 📢 Marketing & Updates
-- Persists preferences in AsyncStorage (`@pezkuwi/email_notifications`)
-- Professional modal design with save/cancel
-
----
-
-### 6. Push Notifications ✅
-
-**Features:**
-- Toggle switch (state only, no actual push setup yet)
-- Ready for expo-notifications integration
-
----
-
-### 7. Terms & Privacy ✅
-
-**Files:**
-- `/home/mamostehp/pwap/mobile/src/components/TermsOfServiceModal.tsx` - EXISTING
-- `/home/mamostehp/pwap/mobile/src/components/PrivacyPolicyModal.tsx` - EXISTING
-
-**Features:**
-- Both modals already existed from Phase 0
-- Connected to Settings buttons
-- Full legal text with Accept button
-
----
-
-### 8. About & Help ✅
-
-**Features:**
-- **About** - Shows app name + version 1.0.0
-- **Help** - Shows support email: support@pezkuwichain.io
-- Simple Alert dialogs
-
----
-
-### 9. Removed Features
-
-**Two-Factor Auth** - Removed (too complex for current scope)
-
----
-
-## Code Quality Improvements
-
-### Fixed Deprecation Warnings
-
-**Issue:** `shadow*" style props are deprecated. Use "boxShadow"`
-
-**Fix:**
-```typescript
-// BEFORE
-shadowColor: '#000',
-shadowOffset: { width: 0, height: 2 },
-shadowOpacity: 0.05,
-shadowRadius: 4,
-
-// AFTER
-boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)',
-```
-
-**Files Fixed:**
-- SettingsScreen.tsx
-
-### Fixed React Native Web Compatibility
-
-**Issue:** Alert.alert() with button arrays doesn't work properly on React Native Web
-
-**Fix:**
-- Created FontSizeModal.tsx to replace Alert-based font size selector
-- Simplified biometric toggle to avoid button arrays
-- Now all interactive elements use proper modals or simple alerts
-
-**Files Fixed:**
-- SettingsScreen.tsx - Replaced Alert.alert() with modal
-- FontSizeModal.tsx - NEW professional font size selector
-
-### Fixed TypeScript Errors
-
-**Issue:** TermsOfServiceModal and PrivacyPolicyModal don't accept `onAccept` prop
-
-**Fix:**
-- Removed `onAccept` prop from modal calls in SettingsScreen
-- Modals now only use `visible` and `onClose` props
-
-**Files Fixed:**
-- SettingsScreen.tsx
-
----
-
-## Files Created
-
-1. `/home/mamostehp/pwap/mobile/src/components/EmailNotificationsModal.tsx` - 350 lines
-2. `/home/mamostehp/pwap/mobile/src/components/ChangePasswordModal.tsx` - 350 lines
-3. `/home/mamostehp/pwap/mobile/src/components/FontSizeModal.tsx` - 200 lines
-
-**Total:** 3 new files, 900 lines of code
-
----
-
-## Files Modified
-
-1. `/home/mamostehp/pwap/shared/theme/colors.ts` - Added DarkColors
-2. `/home/mamostehp/pwap/mobile/src/contexts/ThemeContext.tsx` - Added fontSize + colors
-3. `/home/mamostehp/pwap/mobile/src/contexts/AuthContext.tsx` - Added changePassword + resetPassword
-4. `/home/mamostehp/pwap/mobile/src/screens/SettingsScreen.tsx` - Connected all features
-5. `/home/mamostehp/pwap/mobile/App.tsx` - Added ThemeProvider
-
-**Total:** 5 files modified
-
----
-
-## Settings Screen - Complete Feature List
-
-### APPEARANCE ✅
-- **Dark Mode** - Light/Dark theme toggle
-- **Font Size** - Small/Medium/Large selection
-
-### SECURITY ✅
-- **Biometric Auth** - Fingerprint/Face ID
-- **Change Password** - With current password verification
-
-### NOTIFICATIONS ✅
-- **Push Notifications** - Toggle (ready for implementation)
-- **Email Notifications** - 4 category preferences
-
-### ABOUT ✅
-- **About** - App version info
-- **Terms of Service** - Full legal text modal
-- **Privacy Policy** - Full privacy text modal
-- **Help & Support** - Support email
-
----
-
-## User Experience
-
-### Before Phase 1:
-❌ Dark Mode - Alert "Coming Soon"
-❌ Font Size - Alert with no persistence
-❌ Biometric Auth - Partial implementation
-❌ Change Password - Alert.prompt (doesn't work on Android)
-❌ Email Notifications - Alert "Coming Soon"
-❌ Two-Factor Auth - Alert "Coming Soon"
-
-### After Phase 1:
-✅ Dark Mode - Fully functional, theme changes entire app
-✅ Font Size - 3 sizes, persists, ready for implementation
-✅ Biometric Auth - Fully functional with device hardware
-✅ Change Password - Professional modal with current password verification
-✅ Email Notifications - 4-category modal with persistence
-✅ Push Notifications - Toggle ready
-✅ Terms/Privacy - Full modals
-✅ About/Help - Info displayed
-
----
-
-## Technical Architecture
-
-### State Management
-
-**ThemeContext:**
-```typescript
-{
- isDarkMode: boolean,
- toggleDarkMode: () => Promise,
- colors: LightColors | DarkColors,
- fontSize: 'small' | 'medium' | 'large',
- setFontSize: (size) => Promise,
- fontScale: 0.875 | 1 | 1.125,
-}
-```
-
-**BiometricAuthContext:**
-```typescript
-{
- isBiometricSupported: boolean,
- isBiometricEnrolled: boolean,
- isBiometricAvailable: boolean,
- biometricType: 'fingerprint' | 'facial' | 'iris' | 'none',
- isBiometricEnabled: boolean,
- authenticate: () => Promise,
- enableBiometric: () => Promise,
- disableBiometric: () => Promise,
-}
-```
-
-**AuthContext:**
-```typescript
-{
- user: User | null,
- changePassword: (newPassword, currentPassword) => Promise<{error}>,
- resetPassword: (email) => Promise<{error}>,
-}
-```
-
-### AsyncStorage Keys
-
-- `@pezkuwi/theme` - 'light' | 'dark'
-- `@pezkuwi/font_size` - 'small' | 'medium' | 'large'
-- `@biometric_enabled` - 'true' | 'false'
-- `@pezkuwi/email_notifications` - JSON preferences object
-
----
-
-## Testing Checklist
-
-### Manual Testing:
-
-1. **Dark Mode:**
- - [ ] Toggle ON → Theme changes to dark
- - [ ] Restart app → Theme persists
- - [ ] Toggle OFF → Theme changes to light
-
-2. **Font Size:**
- - [ ] Select Small → Text shrinks
- - [ ] Select Large → Text grows
- - [ ] Restart app → Font size persists
-
-3. **Biometric Auth:**
- - [ ] Toggle ON → Fingerprint prompt appears
- - [ ] Authenticate → Enabled
- - [ ] Toggle OFF → Disabled
-
-4. **Change Password:**
- - [ ] Open modal → 3 inputs visible
- - [ ] Enter wrong current password → Error
- - [ ] Passwords don't match → Error
- - [ ] Valid inputs → Success
- - [ ] Click "Forgot Password" → Email sent
-
-5. **Email Notifications:**
- - [ ] Open modal → 4 categories visible
- - [ ] Toggle switches → State updates
- - [ ] Click Save → Preferences persist
- - [ ] Reopen modal → Toggles show saved state
-
-6. **Terms/Privacy:**
- - [ ] Click Terms → Modal opens with full text
- - [ ] Click Privacy → Modal opens with full text
-
-7. **About/Help:**
- - [ ] Click About → Shows version 1.0.0
- - [ ] Click Help → Shows support email
-
----
-
-## Success Criteria: MET ✅
-
-- ✅ All Settings features functional
-- ✅ No "Coming Soon" alerts
-- ✅ Theme system implemented
-- ✅ Font size system ready
-- ✅ Biometric auth working
-- ✅ Password change with verification
-- ✅ Email preferences modal
-- ✅ Terms/Privacy accessible
-- ✅ Code quality (no deprecated props)
-
----
-
-## Next Steps
-
-**Phase 2:** Finance Features
-- Wallet screen implementation
-- Transfer/Receive modals
-- Transaction history
-- Token management
-
-**Ready to proceed with Phase 2!**
-
----
-
-## Summary
-
-**Phase 1 delivered a FULLY FUNCTIONAL Settings screen.** Every button works, every toggle persists, every modal is professional. No placeholders, no "Coming Soon" alerts.
-
-**Lines of Code Added:** ~700 lines
-**Files Created:** 2 modals
-**Files Modified:** 5 core files
-**Features Delivered:** 10 complete features
-
-**Phase 1: COMPLETE** 🎉
diff --git a/mobile/README.md b/mobile/docs/README.md
similarity index 100%
rename from mobile/README.md
rename to mobile/docs/README.md
diff --git a/mobile/jest.setup.cjs b/mobile/jest.setup.cjs
index 837f4a79..c72756cb 100644
--- a/mobile/jest.setup.cjs
+++ b/mobile/jest.setup.cjs
@@ -194,29 +194,6 @@ jest.mock('../shared/lib/p2p-fiat', () => ({
acceptOffer: jest.fn(() => Promise.resolve(true)),
}));
-// Mock shared i18n module
-jest.mock('../shared/i18n', () => ({
- translations: {
- en: { welcome: 'Welcome' },
- tr: { welcome: 'Hoş geldiniz' },
- kmr: { welcome: 'Bi xêr hatî' },
- ckb: { welcome: 'بەخێربێن' },
- ar: { welcome: 'مرحبا' },
- fa: { welcome: 'خوش آمدید' },
- },
- LANGUAGES: [
- { code: 'en', name: 'English', nativeName: 'English', rtl: false },
- { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', rtl: false },
- { code: 'kmr', name: 'Kurdish Kurmanji', nativeName: 'Kurmancî', rtl: false },
- { code: 'ckb', name: 'Kurdish Sorani', nativeName: 'سۆرانی', rtl: true },
- { code: 'ar', name: 'Arabic', nativeName: 'العربية', rtl: true },
- { code: 'fa', name: 'Persian', nativeName: 'فارسی', rtl: true },
- ],
- DEFAULT_LANGUAGE: 'en',
- LANGUAGE_STORAGE_KEY: '@language',
- isRTL: jest.fn((code) => ['ckb', 'ar', 'fa'].includes(code)),
-}));
-
// Mock shared wallet utilities (handles import.meta)
jest.mock('../shared/lib/wallet', () => ({
formatBalance: jest.fn((amount, decimals) => '0.00'),
@@ -241,33 +218,6 @@ jest.mock('../shared/lib/citizenship-workflow', () => ({
createCitizenshipRequest: jest.fn(() => Promise.resolve({ id: '123' })),
}));
-// Mock react-i18next for i18n initialization
-jest.mock('react-i18next', () => ({
- ...jest.requireActual('react-i18next'),
- useTranslation: () => ({
- t: (key) => key,
- i18n: {
- language: 'en',
- changeLanguage: jest.fn(() => Promise.resolve()),
- isInitialized: true,
- },
- }),
- initReactI18next: {
- type: '3rdParty',
- init: jest.fn(),
- },
-}));
-
-// Mock i18next
-jest.mock('i18next', () => ({
- ...jest.requireActual('i18next'),
- init: jest.fn(() => Promise.resolve()),
- changeLanguage: jest.fn(() => Promise.resolve()),
- use: jest.fn(function () { return this; }),
- language: 'en',
- isInitialized: true,
-}));
-
// Note: Alert is mocked in individual test files where needed
// Silence console warnings in tests
diff --git a/mobile/scripts/reset-wallet.js b/mobile/scripts/reset-wallet.js
new file mode 100644
index 00000000..b7fc20ad
--- /dev/null
+++ b/mobile/scripts/reset-wallet.js
@@ -0,0 +1,31 @@
+/**
+ * Reset Wallet Script
+ *
+ * Clears all wallet data from AsyncStorage for testing purposes.
+ * Run with: node scripts/reset-wallet.js
+ *
+ * Note: This only works in development. For the actual app,
+ * you need to clear the app data or use the in-app reset.
+ */
+
+console.log('='.repeat(50));
+console.log('WALLET RESET INSTRUCTIONS');
+console.log('='.repeat(50));
+console.log('');
+console.log('To reset wallet data in the app, do ONE of these:');
+console.log('');
+console.log('1. Clear App Data (Easiest):');
+console.log(' - iOS Simulator: Device > Erase All Content and Settings');
+console.log(' - Android: Settings > Apps > Pezkuwi > Clear Data');
+console.log(' - Expo Go: Shake device > Clear AsyncStorage');
+console.log('');
+console.log('2. In Expo Go, run this in the console:');
+console.log(' AsyncStorage.multiRemove([');
+console.log(' "@pezkuwi_wallets",');
+console.log(' "@pezkuwi_selected_account",');
+console.log(' "@pezkuwi_selected_network"');
+console.log(' ])');
+console.log('');
+console.log('3. Add temp reset button (already added to Settings)');
+console.log('');
+console.log('='.repeat(50));
diff --git a/mobile/src/__mocks__/contexts/AuthContext.tsx b/mobile/src/__mocks__/contexts/AuthContext.tsx
index ec40fb66..b6790614 100644
--- a/mobile/src/__mocks__/contexts/AuthContext.tsx
+++ b/mobile/src/__mocks__/contexts/AuthContext.tsx
@@ -22,7 +22,7 @@ const mockUser: User = {
created_at: new Date().toISOString(),
};
-const mockAuthContext: AuthContextType = {
+export const mockAuthContext: AuthContextType = {
user: mockUser,
session: null,
loading: false,
diff --git a/mobile/src/__mocks__/contexts/BiometricAuthContext.tsx b/mobile/src/__mocks__/contexts/BiometricAuthContext.tsx
index 2ee71dfa..5c782087 100644
--- a/mobile/src/__mocks__/contexts/BiometricAuthContext.tsx
+++ b/mobile/src/__mocks__/contexts/BiometricAuthContext.tsx
@@ -7,20 +7,36 @@ interface BiometricAuthContextType {
isBiometricAvailable: boolean;
biometricType: 'fingerprint' | 'facial' | 'iris' | 'none';
isBiometricEnabled: boolean;
+ isLocked: boolean;
+ autoLockTimer: number;
authenticate: () => Promise;
enableBiometric: () => Promise;
disableBiometric: () => Promise;
+ setPinCode: (pin: string) => Promise;
+ verifyPinCode: (pin: string) => Promise;
+ setAutoLockTimer: (minutes: number) => Promise;
+ lock: () => void;
+ unlock: () => void;
+ checkAutoLock: () => Promise;
}
-const mockBiometricContext: BiometricAuthContextType = {
+export const mockBiometricContext: BiometricAuthContextType = {
isBiometricSupported: true,
isBiometricEnrolled: true,
isBiometricAvailable: true,
biometricType: 'fingerprint',
isBiometricEnabled: false,
+ isLocked: false,
+ autoLockTimer: 5,
authenticate: jest.fn().mockResolvedValue(true),
enableBiometric: jest.fn().mockResolvedValue(true),
disableBiometric: jest.fn().mockResolvedValue(undefined),
+ setPinCode: jest.fn().mockResolvedValue(undefined),
+ verifyPinCode: jest.fn().mockResolvedValue(true),
+ setAutoLockTimer: jest.fn().mockResolvedValue(undefined),
+ lock: jest.fn(),
+ unlock: jest.fn(),
+ checkAutoLock: jest.fn().mockResolvedValue(undefined),
};
const BiometricAuthContext = createContext(mockBiometricContext);
diff --git a/mobile/src/__mocks__/contexts/ThemeContext.tsx b/mobile/src/__mocks__/contexts/ThemeContext.tsx
index 0eb2f9bc..fa3ec741 100644
--- a/mobile/src/__mocks__/contexts/ThemeContext.tsx
+++ b/mobile/src/__mocks__/contexts/ThemeContext.tsx
@@ -19,7 +19,7 @@ interface ThemeContextType {
fontScale: number;
}
-const mockThemeContext: ThemeContextType = {
+export const mockThemeContext: ThemeContextType = {
isDarkMode: false,
toggleDarkMode: jest.fn().mockResolvedValue(undefined),
colors: LightColors,
diff --git a/mobile/src/__tests__/buttons/ProfileButton.e2e.test.tsx b/mobile/src/__tests__/buttons/ProfileButton.e2e.test.tsx
new file mode 100644
index 00000000..dd4882e2
--- /dev/null
+++ b/mobile/src/__tests__/buttons/ProfileButton.e2e.test.tsx
@@ -0,0 +1,634 @@
+/**
+ * ProfileButton E2E Tests
+ *
+ * Tests the Profile button in BottomTabNavigator and all features
+ * within ProfileScreen and EditProfileScreen.
+ *
+ * Test Coverage:
+ * - Profile screen rendering and loading state
+ * - Profile data display (name, email, avatar)
+ * - Avatar picker modal
+ * - Edit Profile navigation
+ * - About Pezkuwi alert
+ * - Logout flow
+ * - Referrals navigation
+ * - EditProfileScreen rendering
+ * - EditProfileScreen form interactions
+ * - EditProfileScreen save/cancel flows
+ */
+
+import React from 'react';
+import { render, fireEvent, waitFor, act } from '@testing-library/react-native';
+import { Alert } from 'react-native';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+// Mock contexts
+jest.mock('../../contexts/ThemeContext', () => require('../../__mocks__/contexts/ThemeContext'));
+jest.mock('../../contexts/AuthContext', () => require('../../__mocks__/contexts/AuthContext'));
+jest.mock('../../contexts/PezkuwiContext', () => ({
+ usePezkuwi: () => ({
+ endpoint: 'wss://rpc.pezkuwichain.io:9944',
+ setEndpoint: jest.fn(),
+ api: null,
+ isApiReady: false,
+ selectedAccount: null,
+ }),
+}));
+
+// Mock navigation - extended from jest.setup.cjs
+const mockNavigate = jest.fn();
+const mockGoBack = jest.fn();
+jest.mock('@react-navigation/native', () => {
+ const actualNav = jest.requireActual('@react-navigation/native');
+ const ReactModule = require('react');
+ return {
+ ...actualNav,
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ goBack: mockGoBack,
+ setOptions: jest.fn(),
+ addListener: jest.fn(),
+ removeListener: jest.fn(),
+ }),
+ useFocusEffect: (callback: () => (() => void) | void) => {
+ // Use useEffect to properly handle the callback lifecycle
+ ReactModule.useEffect(() => {
+ const unsubscribe = callback();
+ return unsubscribe;
+ }, [callback]);
+ },
+ };
+});
+
+// Mock Alert
+const mockAlert = jest.spyOn(Alert, 'alert');
+
+// Mock Supabase with profile data
+const mockSupabaseFrom = jest.fn();
+const mockProfileData = {
+ id: 'test-user-id',
+ full_name: 'Test User',
+ avatar_url: 'avatar5',
+ wallet_address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
+ created_at: '2026-01-01T00:00:00.000Z',
+ referral_code: 'TESTCODE',
+ referral_count: 5,
+};
+
+jest.mock('../../lib/supabase', () => ({
+ supabase: {
+ from: (table: string) => {
+ mockSupabaseFrom(table);
+ return {
+ select: jest.fn().mockReturnThis(),
+ update: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockReturnThis(),
+ single: jest.fn().mockResolvedValue({
+ data: mockProfileData,
+ error: null,
+ }),
+ };
+ },
+ storage: {
+ from: jest.fn().mockReturnValue({
+ upload: jest.fn().mockResolvedValue({ data: { path: 'test.jpg' }, error: null }),
+ getPublicUrl: jest.fn().mockReturnValue({ data: { publicUrl: 'https://test.com/avatar.jpg' } }),
+ }),
+ },
+ },
+}));
+
+import ProfileScreen from '../../screens/ProfileScreen';
+import EditProfileScreen from '../../screens/EditProfileScreen';
+import { MockThemeProvider, mockThemeContext } from '../../__mocks__/contexts/ThemeContext';
+import { MockAuthProvider, mockAuthContext } from '../../__mocks__/contexts/AuthContext';
+
+// ============================================================
+// TEST HELPERS
+// ============================================================
+
+const renderProfileScreen = (overrides: {
+ theme?: Partial;
+ auth?: Partial;
+} = {}) => {
+ return render(
+
+
+
+
+
+ );
+};
+
+const renderEditProfileScreen = (overrides: {
+ theme?: Partial;
+ auth?: Partial;
+} = {}) => {
+ return render(
+
+
+
+
+
+ );
+};
+
+// ============================================================
+// TESTS
+// ============================================================
+
+describe('ProfileButton E2E Tests', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks();
+ await AsyncStorage.clear();
+ mockAlert.mockClear();
+ mockNavigate.mockClear();
+ mockGoBack.mockClear();
+ });
+
+ // ----------------------------------------------------------
+ // 1. PROFILE SCREEN RENDERING TESTS
+ // ----------------------------------------------------------
+ describe('1. ProfileScreen Rendering', () => {
+ it('renders Profile screen with main container', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-screen')).toBeTruthy();
+ });
+ });
+
+ it('renders header gradient section', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-header-gradient')).toBeTruthy();
+ });
+ });
+
+ it('renders profile cards container', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-cards-container')).toBeTruthy();
+ });
+ });
+
+ it('renders scroll view', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-scroll-view')).toBeTruthy();
+ });
+ });
+
+ it('renders footer with version info', async () => {
+ const { getByTestId, getByText } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-footer')).toBeTruthy();
+ expect(getByText('Version 1.0.0')).toBeTruthy();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 2. PROFILE DATA DISPLAY TESTS
+ // ----------------------------------------------------------
+ describe('2. Profile Data Display', () => {
+ it('displays user name from profile data', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ const nameElement = getByTestId('profile-name');
+ expect(nameElement).toBeTruthy();
+ });
+ });
+
+ it('displays user email', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ const emailElement = getByTestId('profile-email');
+ expect(emailElement).toBeTruthy();
+ });
+ });
+
+ it('displays email card', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-card-email')).toBeTruthy();
+ });
+ });
+
+ it('displays member since card', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-card-member-since')).toBeTruthy();
+ });
+ });
+
+ it('displays referrals card with count', async () => {
+ const { getByTestId, getByText } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-card-referrals')).toBeTruthy();
+ expect(getByText('5 people')).toBeTruthy();
+ });
+ });
+
+ it('displays referral code when available', async () => {
+ const { getByTestId, getByText } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-card-referral-code')).toBeTruthy();
+ expect(getByText('TESTCODE')).toBeTruthy();
+ });
+ });
+
+ it('displays wallet address when available', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-card-wallet')).toBeTruthy();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 3. AVATAR TESTS
+ // ----------------------------------------------------------
+ describe('3. Avatar Display and Interaction', () => {
+ it('renders avatar button', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-avatar-button')).toBeTruthy();
+ });
+ });
+
+ it('displays emoji avatar when avatar_url is emoji ID', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ // avatar5 = 👩🏻
+ expect(getByTestId('profile-avatar-emoji-container')).toBeTruthy();
+ });
+ });
+
+ it('opens avatar picker modal when avatar button is pressed', async () => {
+ const { getByTestId, getByText } = renderProfileScreen();
+
+ await waitFor(() => {
+ const avatarButton = getByTestId('profile-avatar-button');
+ fireEvent.press(avatarButton);
+ });
+
+ await waitFor(() => {
+ // AvatarPickerModal displays "Choose Your Avatar" as title
+ expect(getByText('Choose Your Avatar')).toBeTruthy();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 4. ACTION BUTTONS TESTS
+ // ----------------------------------------------------------
+ describe('4. Action Buttons', () => {
+ it('renders Edit Profile button', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-edit-button')).toBeTruthy();
+ });
+ });
+
+ it('renders About Pezkuwi button', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-about-button')).toBeTruthy();
+ });
+ });
+
+ it('navigates to EditProfile when Edit Profile button is pressed', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ const editButton = getByTestId('profile-edit-button');
+ fireEvent.press(editButton);
+ });
+
+ expect(mockNavigate).toHaveBeenCalledWith('EditProfile');
+ });
+
+ it('shows About Pezkuwi alert when button is pressed', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ const aboutButton = getByTestId('profile-about-button');
+ fireEvent.press(aboutButton);
+ });
+
+ await waitFor(() => {
+ expect(mockAlert).toHaveBeenCalledWith(
+ 'About Pezkuwi',
+ expect.stringContaining('Pezkuwi is a decentralized blockchain platform'),
+ expect.any(Array)
+ );
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 5. REFERRALS NAVIGATION TEST
+ // ----------------------------------------------------------
+ describe('5. Referrals Navigation', () => {
+ it('navigates to Referral screen when referrals card is pressed', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ const referralsCard = getByTestId('profile-card-referrals');
+ fireEvent.press(referralsCard);
+ });
+
+ expect(mockNavigate).toHaveBeenCalledWith('Referral');
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 6. LOGOUT TESTS
+ // ----------------------------------------------------------
+ describe('6. Logout Flow', () => {
+ it('renders logout button', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-logout-button')).toBeTruthy();
+ });
+ });
+
+ it('shows confirmation alert when logout button is pressed', async () => {
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ const logoutButton = getByTestId('profile-logout-button');
+ fireEvent.press(logoutButton);
+ });
+
+ await waitFor(() => {
+ expect(mockAlert).toHaveBeenCalledWith(
+ 'Logout',
+ 'Are you sure you want to logout?',
+ expect.arrayContaining([
+ expect.objectContaining({ text: 'Cancel' }),
+ expect.objectContaining({ text: 'Logout', style: 'destructive' }),
+ ])
+ );
+ });
+ });
+
+ it('calls signOut when logout is confirmed', async () => {
+ const mockSignOut = jest.fn();
+ const { getByTestId } = renderProfileScreen({
+ auth: { signOut: mockSignOut },
+ });
+
+ await waitFor(() => {
+ const logoutButton = getByTestId('profile-logout-button');
+ fireEvent.press(logoutButton);
+ });
+
+ await waitFor(() => {
+ // Get the alert call arguments
+ const alertCall = mockAlert.mock.calls[0];
+ const buttons = alertCall[2];
+ const logoutAction = buttons.find((b: any) => b.text === 'Logout');
+
+ // Simulate pressing Logout
+ if (logoutAction?.onPress) {
+ logoutAction.onPress();
+ }
+ });
+
+ await waitFor(() => {
+ expect(mockSignOut).toHaveBeenCalled();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 7. DARK MODE SUPPORT TESTS
+ // ----------------------------------------------------------
+ describe('7. Dark Mode Support', () => {
+ it('applies dark mode colors when enabled', async () => {
+ const darkColors = {
+ background: '#1A1A1A',
+ surface: '#2A2A2A',
+ text: '#FFFFFF',
+ textSecondary: '#CCCCCC',
+ border: '#404040',
+ };
+
+ const { getByTestId } = renderProfileScreen({
+ theme: { isDarkMode: true, colors: darkColors },
+ });
+
+ await waitFor(() => {
+ expect(getByTestId('profile-screen')).toBeTruthy();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 8. EDIT PROFILE SCREEN RENDERING
+ // ----------------------------------------------------------
+ describe('8. EditProfileScreen Rendering', () => {
+ it('renders EditProfile screen with main container', async () => {
+ const { getByTestId } = renderEditProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('edit-profile-screen')).toBeTruthy();
+ });
+ });
+
+ it('renders header with Cancel and Save buttons', async () => {
+ const { getByTestId, getByText } = renderEditProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('edit-profile-header')).toBeTruthy();
+ expect(getByTestId('edit-profile-cancel-button')).toBeTruthy();
+ expect(getByTestId('edit-profile-save-button')).toBeTruthy();
+ expect(getByText('Edit Profile')).toBeTruthy();
+ });
+ });
+
+ it('renders avatar section', async () => {
+ const { getByTestId, getByText } = renderEditProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('edit-profile-avatar-section')).toBeTruthy();
+ expect(getByText('Change Avatar')).toBeTruthy();
+ });
+ });
+
+ it('renders name input field', async () => {
+ const { getByTestId, getByText } = renderEditProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('edit-profile-name-group')).toBeTruthy();
+ expect(getByTestId('edit-profile-name-input')).toBeTruthy();
+ expect(getByText('Display Name')).toBeTruthy();
+ });
+ });
+
+ it('renders read-only email field', async () => {
+ const { getByTestId, getByText } = renderEditProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('edit-profile-email-group')).toBeTruthy();
+ expect(getByText('Email cannot be changed')).toBeTruthy();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 9. EDIT PROFILE FORM INTERACTIONS
+ // ----------------------------------------------------------
+ describe('9. EditProfileScreen Form Interactions', () => {
+ it('allows editing name field', async () => {
+ const { getByTestId } = renderEditProfileScreen();
+
+ await waitFor(() => {
+ const nameInput = getByTestId('edit-profile-name-input');
+ fireEvent.changeText(nameInput, 'New Name');
+ });
+ });
+
+ it('opens avatar modal when avatar button is pressed', async () => {
+ const { getByTestId, getByText } = renderEditProfileScreen();
+
+ await waitFor(() => {
+ const avatarButton = getByTestId('edit-profile-avatar-button');
+ fireEvent.press(avatarButton);
+ });
+
+ await waitFor(() => {
+ expect(getByText('Change Avatar')).toBeTruthy();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 10. EDIT PROFILE CANCEL FLOW
+ // ----------------------------------------------------------
+ describe('10. EditProfileScreen Cancel Flow', () => {
+ it('goes back without alert when no changes made', async () => {
+ const { getByTestId } = renderEditProfileScreen();
+
+ await waitFor(() => {
+ const cancelButton = getByTestId('edit-profile-cancel-button');
+ fireEvent.press(cancelButton);
+ });
+
+ // Should navigate back directly without showing alert
+ await waitFor(() => {
+ expect(mockGoBack).toHaveBeenCalled();
+ });
+ });
+
+ it('shows discard alert when changes exist', async () => {
+ const { getByTestId } = renderEditProfileScreen();
+
+ await waitFor(async () => {
+ // Make a change
+ const nameInput = getByTestId('edit-profile-name-input');
+ fireEvent.changeText(nameInput, 'Changed Name');
+
+ // Try to cancel
+ const cancelButton = getByTestId('edit-profile-cancel-button');
+ fireEvent.press(cancelButton);
+ });
+
+ await waitFor(() => {
+ expect(mockAlert).toHaveBeenCalledWith(
+ 'Discard Changes?',
+ 'You have unsaved changes. Are you sure you want to go back?',
+ expect.arrayContaining([
+ expect.objectContaining({ text: 'Keep Editing' }),
+ expect.objectContaining({ text: 'Discard', style: 'destructive' }),
+ ])
+ );
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 11. EDIT PROFILE SAVE FLOW
+ // ----------------------------------------------------------
+ describe('11. EditProfileScreen Save Flow', () => {
+ it('Save button is disabled when no changes', async () => {
+ const { getByTestId } = renderEditProfileScreen();
+
+ await waitFor(() => {
+ const saveButton = getByTestId('edit-profile-save-button');
+ expect(saveButton).toBeTruthy();
+ // Save button should have disabled styling when no changes
+ });
+ });
+
+ it('enables Save button when changes are made', async () => {
+ const { getByTestId } = renderEditProfileScreen();
+
+ await waitFor(async () => {
+ // Make a change
+ const nameInput = getByTestId('edit-profile-name-input');
+ fireEvent.changeText(nameInput, 'New Name Here');
+
+ // Save button should now be enabled
+ const saveButton = getByTestId('edit-profile-save-button');
+ expect(saveButton).toBeTruthy();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 12. EDGE CASES
+ // ----------------------------------------------------------
+ describe('12. Edge Cases', () => {
+ it('handles user without profile data gracefully', async () => {
+ // Override mock to return null profile
+ jest.doMock('../../lib/supabase', () => ({
+ supabase: {
+ from: () => ({
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockReturnThis(),
+ single: jest.fn().mockResolvedValue({
+ data: null,
+ error: null,
+ }),
+ }),
+ },
+ }));
+
+ const { getByTestId } = renderProfileScreen();
+
+ await waitFor(() => {
+ expect(getByTestId('profile-screen')).toBeTruthy();
+ });
+ });
+
+ it('displays fallback for missing user email', async () => {
+ const { getByTestId } = renderProfileScreen({
+ auth: { user: null },
+ });
+
+ // Should handle gracefully
+ await waitFor(() => {
+ // Loading state or screen should render
+ });
+ });
+ });
+});
diff --git a/mobile/src/__tests__/buttons/SettingsButton.e2e.test.tsx b/mobile/src/__tests__/buttons/SettingsButton.e2e.test.tsx
new file mode 100644
index 00000000..cbf0cf9c
--- /dev/null
+++ b/mobile/src/__tests__/buttons/SettingsButton.e2e.test.tsx
@@ -0,0 +1,563 @@
+/**
+ * SettingsButton E2E Tests
+ *
+ * Tests the Settings button in DashboardScreen header and all features
+ * within the SettingsScreen. These tests simulate real user interactions.
+ *
+ * Test Coverage:
+ * - Settings screen rendering
+ * - Dark Mode toggle
+ * - Font Size selection
+ * - Push Notifications toggle
+ * - Email Updates toggle
+ * - Network Node selection
+ * - Biometric Security toggle
+ * - Auto-Lock Timer selection
+ * - Profile editing
+ * - Sign Out flow
+ * - Support links
+ */
+
+import React from 'react';
+import { render, fireEvent, waitFor, act } from '@testing-library/react-native';
+import { Alert, Linking } from 'react-native';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+// Mock contexts
+jest.mock('../../contexts/ThemeContext', () => require('../../__mocks__/contexts/ThemeContext'));
+jest.mock('../../contexts/BiometricAuthContext', () => require('../../__mocks__/contexts/BiometricAuthContext'));
+jest.mock('../../contexts/AuthContext', () => require('../../__mocks__/contexts/AuthContext'));
+jest.mock('../../contexts/PezkuwiContext', () => ({
+ usePezkuwi: () => ({
+ endpoint: 'wss://rpc.pezkuwichain.io:9944',
+ setEndpoint: jest.fn(),
+ api: null,
+ isApiReady: false,
+ selectedAccount: null,
+ }),
+}));
+
+// Mock Alert
+const mockAlert = jest.spyOn(Alert, 'alert');
+
+// Mock Linking
+const mockLinkingOpenURL = jest.spyOn(Linking, 'openURL').mockImplementation(() => Promise.resolve(true));
+
+// Mock Supabase
+jest.mock('../../lib/supabase', () => ({
+ supabase: {
+ from: jest.fn(() => ({
+ select: jest.fn().mockReturnThis(),
+ update: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockReturnThis(),
+ maybeSingle: jest.fn().mockResolvedValue({
+ data: {
+ id: 'test-user-id',
+ full_name: 'Test User',
+ notifications_push: true,
+ notifications_email: true,
+ },
+ error: null,
+ }),
+ })),
+ },
+}));
+
+import SettingsScreen from '../../screens/SettingsScreen';
+import { MockThemeProvider, mockThemeContext } from '../../__mocks__/contexts/ThemeContext';
+import { MockBiometricAuthProvider, mockBiometricContext } from '../../__mocks__/contexts/BiometricAuthContext';
+import { MockAuthProvider, mockAuthContext } from '../../__mocks__/contexts/AuthContext';
+
+// ============================================================
+// TEST HELPERS
+// ============================================================
+
+const renderSettingsScreen = (overrides: {
+ theme?: Partial;
+ biometric?: Partial;
+ auth?: Partial;
+} = {}) => {
+ return render(
+
+
+
+
+
+
+
+ );
+};
+
+// ============================================================
+// TESTS
+// ============================================================
+
+describe('SettingsButton E2E Tests', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks();
+ await AsyncStorage.clear();
+ mockAlert.mockClear();
+ mockLinkingOpenURL.mockClear();
+ });
+
+ // ----------------------------------------------------------
+ // 1. RENDERING TESTS
+ // ----------------------------------------------------------
+ describe('1. Screen Rendering', () => {
+ it('renders Settings screen with all sections', () => {
+ const { getByText, getByTestId } = renderSettingsScreen();
+
+ // Main container
+ expect(getByTestId('settings-screen')).toBeTruthy();
+
+ // Section headers
+ expect(getByText('ACCOUNT')).toBeTruthy();
+ expect(getByText('APP SETTINGS')).toBeTruthy();
+ expect(getByText('NETWORK & SECURITY')).toBeTruthy();
+ expect(getByText('SUPPORT')).toBeTruthy();
+
+ // Header
+ expect(getByText('Settings')).toBeTruthy();
+ });
+
+ it('renders all setting items', () => {
+ const { getByText } = renderSettingsScreen();
+
+ // Account section
+ expect(getByText('Edit Profile')).toBeTruthy();
+ expect(getByText('Wallet Management')).toBeTruthy();
+
+ // App Settings section
+ expect(getByText('Dark Mode')).toBeTruthy();
+ expect(getByText('Font Size')).toBeTruthy();
+ expect(getByText('Push Notifications')).toBeTruthy();
+ expect(getByText('Email Updates')).toBeTruthy();
+
+ // Network & Security section
+ expect(getByText('Network Node')).toBeTruthy();
+ expect(getByText('Biometric Security')).toBeTruthy();
+ expect(getByText('Auto-Lock Timer')).toBeTruthy();
+
+ // Support section
+ expect(getByText('Terms of Service')).toBeTruthy();
+ expect(getByText('Privacy Policy')).toBeTruthy();
+ expect(getByText('Help Center')).toBeTruthy();
+
+ // Logout
+ expect(getByText('Sign Out')).toBeTruthy();
+ });
+
+ it('displays version info in footer', () => {
+ const { getByText } = renderSettingsScreen();
+
+ expect(getByText('Pezkuwi Super App v1.0.0')).toBeTruthy();
+ expect(getByText('© 2026 Digital Kurdistan')).toBeTruthy();
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 2. DARK MODE TESTS
+ // ----------------------------------------------------------
+ describe('2. Dark Mode Toggle', () => {
+ it('shows correct subtitle when dark mode is OFF', () => {
+ const { getByText } = renderSettingsScreen({
+ theme: { isDarkMode: false },
+ });
+
+ expect(getByText('Light theme enabled')).toBeTruthy();
+ });
+
+ it('shows correct subtitle when dark mode is ON', () => {
+ const { getByText } = renderSettingsScreen({
+ theme: { isDarkMode: true },
+ });
+
+ expect(getByText('Dark theme enabled')).toBeTruthy();
+ });
+
+ it('calls toggleDarkMode when switch is toggled', async () => {
+ const mockToggle = jest.fn();
+ const { getByTestId } = renderSettingsScreen({
+ theme: { isDarkMode: false, toggleDarkMode: mockToggle },
+ });
+
+ const darkModeSwitch = getByTestId('dark-mode-switch');
+ fireEvent(darkModeSwitch, 'valueChange', true);
+
+ await waitFor(() => {
+ expect(mockToggle).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 3. FONT SIZE TESTS
+ // ----------------------------------------------------------
+ describe('3. Font Size Selection', () => {
+ it('shows current font size in subtitle', () => {
+ const { getByText } = renderSettingsScreen({
+ theme: { fontSize: 'medium' },
+ });
+
+ expect(getByText('Medium')).toBeTruthy();
+ });
+
+ it('opens font size modal when button is pressed', async () => {
+ const { getByTestId, getByText } = renderSettingsScreen();
+
+ const fontSizeButton = getByTestId('font-size-button');
+ fireEvent.press(fontSizeButton);
+
+ await waitFor(() => {
+ expect(getByText('Select Font Size')).toBeTruthy();
+ expect(getByText('Small')).toBeTruthy();
+ expect(getByText('Large')).toBeTruthy();
+ });
+ });
+
+ it('calls setFontSize when Small option is selected', async () => {
+ const mockSetFontSize = jest.fn();
+ const { getByTestId } = renderSettingsScreen({
+ theme: { fontSize: 'medium', setFontSize: mockSetFontSize },
+ });
+
+ // Open modal
+ fireEvent.press(getByTestId('font-size-button'));
+
+ // Select Small
+ await waitFor(() => {
+ const smallOption = getByTestId('font-size-option-small');
+ fireEvent.press(smallOption);
+ });
+
+ await waitFor(() => {
+ expect(mockSetFontSize).toHaveBeenCalledWith('small');
+ });
+ });
+
+ it('calls setFontSize when Large option is selected', async () => {
+ const mockSetFontSize = jest.fn();
+ const { getByTestId } = renderSettingsScreen({
+ theme: { fontSize: 'medium', setFontSize: mockSetFontSize },
+ });
+
+ // Open modal
+ fireEvent.press(getByTestId('font-size-button'));
+
+ // Select Large
+ await waitFor(() => {
+ const largeOption = getByTestId('font-size-option-large');
+ fireEvent.press(largeOption);
+ });
+
+ await waitFor(() => {
+ expect(mockSetFontSize).toHaveBeenCalledWith('large');
+ });
+ });
+
+ it('closes modal when Cancel is pressed', async () => {
+ const { getByTestId, queryByText } = renderSettingsScreen();
+
+ // Open modal
+ fireEvent.press(getByTestId('font-size-button'));
+
+ // Cancel
+ await waitFor(() => {
+ const cancelButton = getByTestId('font-size-modal-cancel');
+ fireEvent.press(cancelButton);
+ });
+
+ // Modal should close (title should not be visible)
+ await waitFor(() => {
+ // After closing, modal content should not be rendered
+ // This is a basic check - in reality modal visibility is controlled by state
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 4. AUTO-LOCK TIMER TESTS
+ // ----------------------------------------------------------
+ describe('4. Auto-Lock Timer Selection', () => {
+ it('shows current auto-lock time in subtitle', () => {
+ const { getByText } = renderSettingsScreen({
+ biometric: { autoLockTimer: 5 },
+ });
+
+ expect(getByText('5 minutes')).toBeTruthy();
+ });
+
+ it('opens auto-lock modal when button is pressed', async () => {
+ const { getByTestId, getByText } = renderSettingsScreen();
+
+ const autoLockButton = getByTestId('auto-lock-button');
+ fireEvent.press(autoLockButton);
+
+ await waitFor(() => {
+ // Check for modal-specific content
+ expect(getByText('Lock app after inactivity')).toBeTruthy();
+ expect(getByText('1 minute')).toBeTruthy();
+ expect(getByText('15 minutes')).toBeTruthy();
+ });
+ });
+
+ it('calls setAutoLockTimer when option is selected', async () => {
+ const mockSetAutoLock = jest.fn();
+ const { getByTestId } = renderSettingsScreen({
+ biometric: { autoLockTimer: 5, setAutoLockTimer: mockSetAutoLock },
+ });
+
+ // Open modal
+ fireEvent.press(getByTestId('auto-lock-button'));
+
+ // Select 15 minutes
+ await waitFor(() => {
+ const option = getByTestId('auto-lock-option-15');
+ fireEvent.press(option);
+ });
+
+ await waitFor(() => {
+ expect(mockSetAutoLock).toHaveBeenCalledWith(15);
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 5. BIOMETRIC SECURITY TESTS
+ // ----------------------------------------------------------
+ describe('5. Biometric Security Toggle', () => {
+ it('shows "FaceID / Fingerprint" when biometric is disabled', () => {
+ const { getByText } = renderSettingsScreen({
+ biometric: { isBiometricEnabled: false },
+ });
+
+ expect(getByText('FaceID / Fingerprint')).toBeTruthy();
+ });
+
+ it('shows biometric type when enabled', () => {
+ const { getByText } = renderSettingsScreen({
+ biometric: { isBiometricEnabled: true, biometricType: 'fingerprint' },
+ });
+
+ expect(getByText('Enabled (fingerprint)')).toBeTruthy();
+ });
+
+ it('calls enableBiometric when toggled ON', async () => {
+ const mockEnable = jest.fn().mockResolvedValue(true);
+ const { getByTestId } = renderSettingsScreen({
+ biometric: { isBiometricEnabled: false, enableBiometric: mockEnable },
+ });
+
+ const biometricSwitch = getByTestId('biometric-security-switch');
+ fireEvent(biometricSwitch, 'valueChange', true);
+
+ await waitFor(() => {
+ expect(mockEnable).toHaveBeenCalled();
+ });
+ });
+
+ it('calls disableBiometric when toggled OFF', async () => {
+ const mockDisable = jest.fn();
+ const { getByTestId } = renderSettingsScreen({
+ biometric: { isBiometricEnabled: true, disableBiometric: mockDisable },
+ });
+
+ const biometricSwitch = getByTestId('biometric-security-switch');
+ fireEvent(biometricSwitch, 'valueChange', false);
+
+ await waitFor(() => {
+ expect(mockDisable).toHaveBeenCalled();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 6. NETWORK NODE TESTS
+ // ----------------------------------------------------------
+ describe('6. Network Node Selection', () => {
+ it('shows Mainnet in subtitle for production endpoint', () => {
+ const { getByText } = renderSettingsScreen();
+
+ expect(getByText('Mainnet')).toBeTruthy();
+ });
+
+ it('opens network modal when button is pressed', async () => {
+ const { getByTestId, getByText } = renderSettingsScreen();
+
+ const networkButton = getByTestId('network-node-button');
+ fireEvent.press(networkButton);
+
+ await waitFor(() => {
+ expect(getByText('Select Network Node')).toBeTruthy();
+ expect(getByText('Pezkuwi Mainnet')).toBeTruthy();
+ expect(getByText('Pezkuwi Testnet')).toBeTruthy();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 7. SIGN OUT TESTS
+ // ----------------------------------------------------------
+ describe('7. Sign Out Flow', () => {
+ it('shows confirmation alert when Sign Out is pressed', async () => {
+ const { getByTestId } = renderSettingsScreen();
+
+ const signOutButton = getByTestId('sign-out-button');
+ fireEvent.press(signOutButton);
+
+ await waitFor(() => {
+ expect(mockAlert).toHaveBeenCalledWith(
+ 'Sign Out',
+ 'Are you sure you want to sign out?',
+ expect.arrayContaining([
+ expect.objectContaining({ text: 'Cancel' }),
+ expect.objectContaining({ text: 'Sign Out', style: 'destructive' }),
+ ])
+ );
+ });
+ });
+
+ it('calls signOut when confirmed', async () => {
+ const mockSignOut = jest.fn();
+ const { getByTestId } = renderSettingsScreen({
+ auth: { signOut: mockSignOut },
+ });
+
+ const signOutButton = getByTestId('sign-out-button');
+ fireEvent.press(signOutButton);
+
+ await waitFor(() => {
+ // Get the alert call arguments
+ const alertCall = mockAlert.mock.calls[0];
+ const buttons = alertCall[2];
+ const signOutAction = buttons.find((b: any) => b.text === 'Sign Out');
+
+ // Simulate pressing Sign Out
+ if (signOutAction?.onPress) {
+ signOutAction.onPress();
+ }
+
+ expect(mockSignOut).toHaveBeenCalled();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 8. SUPPORT LINKS TESTS
+ // ----------------------------------------------------------
+ describe('8. Support Links', () => {
+ it('shows Terms of Service alert when pressed', async () => {
+ const { getByTestId } = renderSettingsScreen();
+
+ const tosButton = getByTestId('terms-of-service-button');
+ fireEvent.press(tosButton);
+
+ await waitFor(() => {
+ expect(mockAlert).toHaveBeenCalledWith(
+ 'Terms',
+ 'Terms of service content...'
+ );
+ });
+ });
+
+ it('shows Privacy Policy alert when pressed', async () => {
+ const { getByTestId } = renderSettingsScreen();
+
+ const privacyButton = getByTestId('privacy-policy-button');
+ fireEvent.press(privacyButton);
+
+ await waitFor(() => {
+ expect(mockAlert).toHaveBeenCalledWith(
+ 'Privacy',
+ 'Privacy policy content...'
+ );
+ });
+ });
+
+ it('opens email client when Help Center is pressed', async () => {
+ const { getByTestId } = renderSettingsScreen();
+
+ const helpButton = getByTestId('help-center-button');
+ fireEvent.press(helpButton);
+
+ await waitFor(() => {
+ expect(mockLinkingOpenURL).toHaveBeenCalledWith(
+ 'mailto:support@pezkuwichain.io'
+ );
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 9. PROFILE EDIT TESTS
+ // ----------------------------------------------------------
+ describe('9. Profile Editing', () => {
+ it('opens profile edit modal when Edit Profile is pressed', async () => {
+ const { getByTestId, getByText } = renderSettingsScreen();
+
+ const editProfileButton = getByTestId('edit-profile-button');
+ fireEvent.press(editProfileButton);
+
+ await waitFor(() => {
+ // Check for modal-specific content (Full Name and Bio labels)
+ expect(getByText('Full Name')).toBeTruthy();
+ expect(getByText('Bio')).toBeTruthy();
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 10. WALLET MANAGEMENT TESTS
+ // ----------------------------------------------------------
+ describe('10. Wallet Management', () => {
+ it('shows Coming Soon alert when Wallet Management is pressed', async () => {
+ const { getByTestId } = renderSettingsScreen();
+
+ const walletButton = getByTestId('wallet-management-button');
+ fireEvent.press(walletButton);
+
+ await waitFor(() => {
+ expect(mockAlert).toHaveBeenCalledWith(
+ 'Coming Soon',
+ 'Wallet management screen'
+ );
+ });
+ });
+ });
+
+ // ----------------------------------------------------------
+ // 11. EDGE CASES
+ // ----------------------------------------------------------
+ describe('11. Edge Cases', () => {
+ it('handles rapid toggle clicks gracefully', async () => {
+ const mockToggle = jest.fn();
+ const { getByTestId } = renderSettingsScreen({
+ theme: { isDarkMode: false, toggleDarkMode: mockToggle },
+ });
+
+ const darkModeSwitch = getByTestId('dark-mode-switch');
+
+ // Rapid clicks
+ fireEvent(darkModeSwitch, 'valueChange', true);
+ fireEvent(darkModeSwitch, 'valueChange', false);
+ fireEvent(darkModeSwitch, 'valueChange', true);
+
+ await waitFor(() => {
+ expect(mockToggle).toHaveBeenCalledTimes(3);
+ });
+ });
+
+ it('displays correctly with all toggles enabled', () => {
+ const { getByTestId } = renderSettingsScreen({
+ theme: { isDarkMode: true },
+ biometric: { isBiometricEnabled: true, biometricType: 'facial' },
+ });
+
+ // All toggles should be visible
+ expect(getByTestId('dark-mode-switch')).toBeTruthy();
+ expect(getByTestId('biometric-security-switch')).toBeTruthy();
+ expect(getByTestId('push-notifications-switch')).toBeTruthy();
+ expect(getByTestId('email-updates-switch')).toBeTruthy();
+ });
+ });
+});
diff --git a/mobile/src/__tests__/buttons/WalletButton.e2e.test.tsx b/mobile/src/__tests__/buttons/WalletButton.e2e.test.tsx
new file mode 100644
index 00000000..2117f627
--- /dev/null
+++ b/mobile/src/__tests__/buttons/WalletButton.e2e.test.tsx
@@ -0,0 +1,179 @@
+/**
+ * WalletButton E2E Tests
+ *
+ * Tests the Wallet button flow including:
+ * - WalletSetupScreen choice screen
+ * - Basic navigation
+ */
+
+import React from 'react';
+import { render, fireEvent, waitFor } from '@testing-library/react-native';
+import { Alert } from 'react-native';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+// Mock contexts
+jest.mock('../../contexts/ThemeContext', () => require('../../__mocks__/contexts/ThemeContext'));
+jest.mock('../../contexts/AuthContext', () => require('../../__mocks__/contexts/AuthContext'));
+
+jest.mock('../../contexts/PezkuwiContext', () => ({
+ usePezkuwi: () => ({
+ api: null,
+ isApiReady: false,
+ accounts: [],
+ selectedAccount: null,
+ connectWallet: jest.fn().mockResolvedValue(undefined),
+ disconnectWallet: jest.fn(),
+ createWallet: jest.fn().mockResolvedValue({
+ address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
+ mnemonic: 'test test test test test test test test test test test junk',
+ }),
+ importWallet: jest.fn().mockResolvedValue({
+ address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
+ }),
+ getKeyPair: jest.fn(),
+ currentNetwork: 'mainnet',
+ switchNetwork: jest.fn(),
+ error: null,
+ }),
+ NetworkType: { MAINNET: 'mainnet' },
+ NETWORKS: { mainnet: { displayName: 'Mainnet', endpoint: 'wss://mainnet.example.com' } },
+}));
+
+// Mock @pezkuwi/util-crypto
+jest.mock('@pezkuwi/util-crypto', () => ({
+ mnemonicGenerate: () => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
+ mnemonicValidate: () => true,
+ cryptoWaitReady: jest.fn().mockResolvedValue(true),
+}));
+
+// Mock navigation
+const mockGoBack = jest.fn();
+const mockReplace = jest.fn();
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: () => ({
+ navigate: jest.fn(),
+ goBack: mockGoBack,
+ replace: mockReplace,
+ setOptions: jest.fn(),
+ }),
+}));
+
+// Mock Alert
+jest.spyOn(Alert, 'alert');
+
+import WalletSetupScreen from '../../screens/WalletSetupScreen';
+import { MockThemeProvider } from '../../__mocks__/contexts/ThemeContext';
+import { MockAuthProvider } from '../../__mocks__/contexts/AuthContext';
+
+const renderSetup = () => render(
+
+
+
+
+
+);
+
+describe('WalletSetupScreen', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders choice screen', async () => {
+ const { getByTestId, getByText } = renderSetup();
+ await waitFor(() => {
+ expect(getByTestId('wallet-setup-screen')).toBeTruthy();
+ expect(getByText('Set Up Your Wallet')).toBeTruthy();
+ });
+ });
+
+ it('shows create button', async () => {
+ const { getByTestId, getByText } = renderSetup();
+ await waitFor(() => {
+ expect(getByTestId('wallet-setup-create-button')).toBeTruthy();
+ expect(getByText('Create New Wallet')).toBeTruthy();
+ });
+ });
+
+ it('shows import button', async () => {
+ const { getByTestId, getByText } = renderSetup();
+ await waitFor(() => {
+ expect(getByTestId('wallet-setup-import-button')).toBeTruthy();
+ expect(getByText('Import Existing Wallet')).toBeTruthy();
+ });
+ });
+
+ it('close button calls goBack', async () => {
+ const { getByTestId } = renderSetup();
+ await waitFor(() => {
+ fireEvent.press(getByTestId('wallet-setup-close'));
+ });
+ expect(mockGoBack).toHaveBeenCalled();
+ });
+
+ it('create button shows seed phrase screen', async () => {
+ const { getByTestId, getByText } = renderSetup();
+ await waitFor(() => {
+ fireEvent.press(getByTestId('wallet-setup-create-button'));
+ });
+ await waitFor(() => {
+ expect(getByText('Your Recovery Phrase')).toBeTruthy();
+ });
+ });
+
+ it('import button shows import screen', async () => {
+ const { getByTestId, getByText } = renderSetup();
+ await waitFor(() => {
+ fireEvent.press(getByTestId('wallet-setup-import-button'));
+ });
+ await waitFor(() => {
+ expect(getByText('Import Wallet')).toBeTruthy();
+ });
+ });
+
+ it('seed phrase screen has mnemonic grid', async () => {
+ const { getByTestId } = renderSetup();
+ await waitFor(() => {
+ fireEvent.press(getByTestId('wallet-setup-create-button'));
+ });
+ await waitFor(() => {
+ expect(getByTestId('mnemonic-grid')).toBeTruthy();
+ });
+ });
+
+ it('import screen has input field', async () => {
+ const { getByTestId } = renderSetup();
+ await waitFor(() => {
+ fireEvent.press(getByTestId('wallet-setup-import-button'));
+ });
+ await waitFor(() => {
+ expect(getByTestId('wallet-import-input')).toBeTruthy();
+ });
+ });
+
+ it('back from seed phrase goes to choice', async () => {
+ const { getByTestId } = renderSetup();
+ await waitFor(() => {
+ fireEvent.press(getByTestId('wallet-setup-create-button'));
+ });
+ await waitFor(() => {
+ fireEvent.press(getByTestId('wallet-setup-back'));
+ });
+ await waitFor(() => {
+ expect(getByTestId('wallet-setup-choice')).toBeTruthy();
+ });
+ });
+
+ it('back from import goes to choice', async () => {
+ const { getByTestId } = renderSetup();
+ await waitFor(() => {
+ fireEvent.press(getByTestId('wallet-setup-import-button'));
+ });
+ await waitFor(() => {
+ fireEvent.press(getByTestId('wallet-setup-back'));
+ });
+ await waitFor(() => {
+ expect(getByTestId('wallet-setup-choice')).toBeTruthy();
+ });
+ });
+});
diff --git a/mobile/src/__tests__/screens/SettingsScreen.DarkMode.test.tsx b/mobile/src/__tests__/screens/SettingsScreen.DarkMode.test.tsx
deleted file mode 100644
index d56bd578..00000000
--- a/mobile/src/__tests__/screens/SettingsScreen.DarkMode.test.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-import React from 'react';
-import { render, fireEvent, waitFor } from '@testing-library/react-native';
-import AsyncStorage from '@react-native-async-storage/async-storage';
-
-// Mock contexts before importing SettingsScreen
-jest.mock('../../contexts/ThemeContext', () => require('../../__mocks__/contexts/ThemeContext'));
-jest.mock('../../contexts/BiometricAuthContext', () => require('../../__mocks__/contexts/BiometricAuthContext'));
-jest.mock('../../contexts/AuthContext', () => require('../../__mocks__/contexts/AuthContext'));
-
-// Mock Alert
-jest.mock('react-native/Libraries/Alert/Alert', () => ({
- alert: jest.fn(),
-}));
-
-import SettingsScreen from '../../screens/SettingsScreen';
-import { MockThemeProvider } from '../../__mocks__/contexts/ThemeContext';
-import { MockBiometricAuthProvider } from '../../__mocks__/contexts/BiometricAuthContext';
-import { MockAuthProvider } from '../../__mocks__/contexts/AuthContext';
-
-// Helper to render SettingsScreen with all required providers
-const renderSettingsScreen = (themeValue = {}, biometricValue = {}, authValue = {}) => {
- return render(
-
-
-
-
-
-
-
- );
-};
-
-describe('SettingsScreen - Dark Mode Feature', () => {
- beforeEach(async () => {
- jest.clearAllMocks();
- // Clear AsyncStorage before each test
- await AsyncStorage.clear();
- });
-
- describe('Rendering', () => {
- it('should render Dark Mode section with toggle', () => {
- const { getByText } = renderSettingsScreen();
-
- expect(getByText('APPEARANCE')).toBeTruthy();
- expect(getByText('darkMode')).toBeTruthy();
- });
-
- it('should show current dark mode state', () => {
- const { getByText } = renderSettingsScreen({ isDarkMode: false });
-
- // Should show subtitle when dark mode is off
- expect(getByText(/settingsScreen.subtitles.lightThemeEnabled/i)).toBeTruthy();
- });
-
- it('should show "Enabled" when dark mode is on', () => {
- const { getByText } = renderSettingsScreen({ isDarkMode: true });
-
- // Should show subtitle when dark mode is on
- expect(getByText(/settingsScreen.subtitles.darkThemeEnabled/i)).toBeTruthy();
- });
- });
-
- describe('Toggle Functionality', () => {
- it('should toggle dark mode when switch is pressed', async () => {
- const mockToggleDarkMode = jest.fn().mockResolvedValue(undefined);
- const { getByTestId } = renderSettingsScreen({
- isDarkMode: false,
- toggleDarkMode: mockToggleDarkMode,
- });
-
- // Find the toggle switch
- const darkModeSwitch = getByTestId('dark-mode-switch');
-
- // Toggle the switch
- fireEvent(darkModeSwitch, 'valueChange', true);
-
- // Verify toggleDarkMode was called
- await waitFor(() => {
- expect(mockToggleDarkMode).toHaveBeenCalledTimes(1);
- });
- });
-
- it('should toggle from enabled to disabled', async () => {
- const mockToggleDarkMode = jest.fn().mockResolvedValue(undefined);
- const { getByTestId } = renderSettingsScreen({
- isDarkMode: true,
- toggleDarkMode: mockToggleDarkMode,
- });
-
- const darkModeSwitch = getByTestId('dark-mode-switch');
-
- // Toggle off
- fireEvent(darkModeSwitch, 'valueChange', false);
-
- await waitFor(() => {
- expect(mockToggleDarkMode).toHaveBeenCalledTimes(1);
- });
- });
- });
-
- describe('Persistence', () => {
- it('should save dark mode preference to AsyncStorage', async () => {
- const mockToggleDarkMode = jest.fn(async () => {
- await AsyncStorage.setItem('@pezkuwi/theme', 'dark');
- });
-
- const { getByTestId } = renderSettingsScreen({
- isDarkMode: false,
- toggleDarkMode: mockToggleDarkMode,
- });
-
- const darkModeSwitch = getByTestId('dark-mode-switch');
- fireEvent(darkModeSwitch, 'valueChange', true);
-
- await waitFor(() => {
- expect(AsyncStorage.setItem).toHaveBeenCalledWith('@pezkuwi/theme', 'dark');
- });
- });
-
- it('should load dark mode preference on mount', async () => {
- // Pre-set dark mode in AsyncStorage
- await AsyncStorage.setItem('@pezkuwi/theme', 'dark');
-
- const { getByText } = renderSettingsScreen({ isDarkMode: true });
-
- // Verify dark mode is enabled - check for dark theme subtitle
- expect(getByText(/settingsScreen.subtitles.darkThemeEnabled/i)).toBeTruthy();
- });
- });
-
- describe('Theme Application', () => {
- it('should apply dark theme colors when enabled', () => {
- const darkColors = {
- background: '#121212',
- surface: '#1E1E1E',
- text: '#FFFFFF',
- textSecondary: '#B0B0B0',
- border: '#333333',
- };
-
- const { getByTestId } = renderSettingsScreen({
- isDarkMode: true,
- colors: darkColors,
- });
-
- const container = getByTestId('settings-screen');
-
- // Verify dark background is applied
- expect(container.props.style).toEqual(
- expect.objectContaining({
- backgroundColor: darkColors.background,
- })
- );
- });
-
- it('should apply light theme colors when disabled', () => {
- const lightColors = {
- background: '#F5F5F5',
- surface: '#FFFFFF',
- text: '#000000',
- textSecondary: '#666666',
- border: '#E0E0E0',
- };
-
- const { getByTestId } = renderSettingsScreen({
- isDarkMode: false,
- colors: lightColors,
- });
-
- const container = getByTestId('settings-screen');
-
- // Verify light background is applied
- expect(container.props.style).toEqual(
- expect.objectContaining({
- backgroundColor: lightColors.background,
- })
- );
- });
- });
-
- describe('Edge Cases', () => {
- it('should handle rapid toggle clicks', async () => {
- const mockToggleDarkMode = jest.fn().mockResolvedValue(undefined);
- const { getByTestId } = renderSettingsScreen({
- isDarkMode: false,
- toggleDarkMode: mockToggleDarkMode,
- });
-
- const darkModeSwitch = getByTestId('dark-mode-switch');
-
- // Rapid clicks
- fireEvent(darkModeSwitch, 'valueChange', true);
- fireEvent(darkModeSwitch, 'valueChange', false);
- fireEvent(darkModeSwitch, 'valueChange', true);
-
- await waitFor(() => {
- // Should handle all toggle attempts
- expect(mockToggleDarkMode).toHaveBeenCalled();
- });
- });
-
- it('should call toggleDarkMode multiple times without issues', async () => {
- const mockToggleDarkMode = jest.fn().mockResolvedValue(undefined);
- const { getByTestId } = renderSettingsScreen({
- isDarkMode: false,
- toggleDarkMode: mockToggleDarkMode,
- });
-
- const darkModeSwitch = getByTestId('dark-mode-switch');
-
- // Toggle multiple times
- fireEvent(darkModeSwitch, 'valueChange', true);
- fireEvent(darkModeSwitch, 'valueChange', false);
- fireEvent(darkModeSwitch, 'valueChange', true);
-
- await waitFor(() => {
- // Should handle all calls
- expect(mockToggleDarkMode).toHaveBeenCalledTimes(3);
- });
- });
- });
-});
diff --git a/mobile/src/__tests__/screens/SettingsScreen.FontSize.test.tsx b/mobile/src/__tests__/screens/SettingsScreen.FontSize.test.tsx
deleted file mode 100644
index 29fc121a..00000000
--- a/mobile/src/__tests__/screens/SettingsScreen.FontSize.test.tsx
+++ /dev/null
@@ -1,240 +0,0 @@
-import React from 'react';
-import { render, fireEvent, waitFor } from '@testing-library/react-native';
-import AsyncStorage from '@react-native-async-storage/async-storage';
-
-// Mock contexts before importing SettingsScreen
-jest.mock('../../contexts/ThemeContext', () => require('../../__mocks__/contexts/ThemeContext'));
-jest.mock('../../contexts/BiometricAuthContext', () => require('../../__mocks__/contexts/BiometricAuthContext'));
-jest.mock('../../contexts/AuthContext', () => require('../../__mocks__/contexts/AuthContext'));
-
-// Mock Alert
-jest.mock('react-native/Libraries/Alert/Alert', () => ({
- alert: jest.fn(),
-}));
-
-import SettingsScreen from '../../screens/SettingsScreen';
-import FontSizeModal from '../../components/FontSizeModal';
-import { MockThemeProvider } from '../../__mocks__/contexts/ThemeContext';
-import { MockBiometricAuthProvider } from '../../__mocks__/contexts/BiometricAuthContext';
-import { MockAuthProvider } from '../../__mocks__/contexts/AuthContext';
-
-// Helper to render SettingsScreen with all required providers
-const renderSettingsScreen = (themeValue = {}, biometricValue = {}, authValue = {}) => {
- return render(
-
-
-
-
-
-
-
- );
-};
-
-// Helper to render FontSizeModal
-const renderFontSizeModal = (overrides: any = {}) => {
- const mockSetFontSize = overrides.setFontSize || jest.fn().mockResolvedValue(undefined);
- const mockOnClose = overrides.onClose || jest.fn();
-
- const themeValue = {
- fontSize: overrides.fontSize || ('medium' as 'small' | 'medium' | 'large'),
- setFontSize: mockSetFontSize,
- };
-
- const props = {
- visible: overrides.visible !== undefined ? overrides.visible : true,
- onClose: mockOnClose,
- };
-
- return {
- ...render(
-
-
-
- ),
- mockSetFontSize,
- mockOnClose,
- };
-};
-
-describe('SettingsScreen - Font Size Feature', () => {
- beforeEach(async () => {
- jest.clearAllMocks();
- await AsyncStorage.clear();
- });
-
- describe('Rendering', () => {
- it('should render Font Size button', () => {
- const { getByText } = renderSettingsScreen();
-
- expect(getByText('Font Size')).toBeTruthy();
- });
-
- it('should show current font size in subtitle', () => {
- const { getByText } = renderSettingsScreen({ fontSize: 'medium' });
-
- expect(getByText('Current: Medium')).toBeTruthy();
- });
-
- it('should show Small font size in subtitle', () => {
- const { getByText } = renderSettingsScreen({ fontSize: 'small' });
-
- expect(getByText('Current: Small')).toBeTruthy();
- });
-
- it('should show Large font size in subtitle', () => {
- const { getByText } = renderSettingsScreen({ fontSize: 'large' });
-
- expect(getByText('Current: Large')).toBeTruthy();
- });
- });
-
- describe('Modal Interaction', () => {
- it('should open font size modal when button is pressed', async () => {
- const { getByText, getByTestId } = renderSettingsScreen();
-
- const fontSizeButton = getByText('Font Size').parent?.parent;
- expect(fontSizeButton).toBeTruthy();
-
- fireEvent.press(fontSizeButton!);
-
- // Modal should open (we'll test modal rendering separately)
- await waitFor(() => {
- // Just verify the button was pressable
- expect(fontSizeButton).toBeTruthy();
- });
- });
- });
-
- describe('Font Scale Application', () => {
- it('should display small font scale', () => {
- const { getByText } = renderSettingsScreen({
- fontSize: 'small',
- fontScale: 0.875,
- });
-
- // Verify font size is displayed
- expect(getByText('Current: Small')).toBeTruthy();
- });
-
- it('should display medium font scale', () => {
- const { getByText } = renderSettingsScreen({
- fontSize: 'medium',
- fontScale: 1.0,
- });
-
- expect(getByText('Current: Medium')).toBeTruthy();
- });
-
- it('should display large font scale', () => {
- const { getByText } = renderSettingsScreen({
- fontSize: 'large',
- fontScale: 1.125,
- });
-
- expect(getByText('Current: Large')).toBeTruthy();
- });
- });
-
- describe('Persistence', () => {
- it('should save font size to AsyncStorage', async () => {
- const mockSetFontSize = jest.fn(async (size) => {
- await AsyncStorage.setItem('@pezkuwi/font_size', size);
- });
-
- const { getByText } = renderSettingsScreen({
- fontSize: 'medium',
- setFontSize: mockSetFontSize,
- });
-
- // Simulate selecting a new size
- await mockSetFontSize('large');
-
- await waitFor(() => {
- expect(AsyncStorage.setItem).toHaveBeenCalledWith('@pezkuwi/font_size', 'large');
- });
- });
-
- it('should load saved font size on mount', async () => {
- await AsyncStorage.setItem('@pezkuwi/font_size', 'large');
-
- const { getByText } = renderSettingsScreen({ fontSize: 'large' });
-
- expect(getByText('Current: Large')).toBeTruthy();
- });
- });
-});
-
-describe('FontSizeModal Component', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- describe('Rendering', () => {
- it('should render modal when visible', () => {
- const { getByText } = renderFontSizeModal({ fontSize: 'medium', visible: true });
-
- expect(getByText('Font Size')).toBeTruthy();
- });
-
- it('should render all three size options', () => {
- const { getByText } = renderFontSizeModal();
-
- expect(getByText('Small')).toBeTruthy();
- expect(getByText(/Medium.*Default/i)).toBeTruthy();
- expect(getByText('Large')).toBeTruthy();
- });
-
- it('should show checkmark on current size', () => {
- const { getByTestId, getByText } = renderFontSizeModal({ fontSize: 'medium' });
-
- const mediumOption = getByTestId('font-size-medium');
- expect(mediumOption).toBeTruthy();
- // Checkmark should be visible for medium
- expect(getByText('✓')).toBeTruthy();
- });
- });
-
- describe('Size Selection', () => {
- it('should call setFontSize when Small is pressed', async () => {
- const { getByTestId, mockSetFontSize, mockOnClose } = renderFontSizeModal({
- fontSize: 'medium',
- });
-
- const smallButton = getByTestId('font-size-small');
- fireEvent.press(smallButton);
-
- await waitFor(() => {
- expect(mockSetFontSize).toHaveBeenCalledWith('small');
- expect(mockOnClose).toHaveBeenCalled();
- });
- });
-
- it('should call setFontSize when Large is pressed', async () => {
- const { getByTestId, mockSetFontSize, mockOnClose } = renderFontSizeModal({
- fontSize: 'medium',
- });
-
- const largeButton = getByTestId('font-size-large');
- fireEvent.press(largeButton);
-
- await waitFor(() => {
- expect(mockSetFontSize).toHaveBeenCalledWith('large');
- expect(mockOnClose).toHaveBeenCalled();
- });
- });
- });
-
- describe('Modal Close', () => {
- it('should call onClose when close button is pressed', async () => {
- const { getByTestId, mockOnClose } = renderFontSizeModal();
-
- const closeButton = getByTestId('font-size-modal-close');
- fireEvent.press(closeButton);
-
- await waitFor(() => {
- expect(mockOnClose).toHaveBeenCalled();
- });
- });
- });
-});
diff --git a/mobile/src/__tests__/test-utils.tsx b/mobile/src/__tests__/test-utils.tsx
index 1430606c..7f4dccb2 100644
--- a/mobile/src/__tests__/test-utils.tsx
+++ b/mobile/src/__tests__/test-utils.tsx
@@ -4,7 +4,6 @@ import { render, RenderOptions } from '@testing-library/react-native';
// Mock all contexts with simple implementations
const MockAuthProvider = ({ children }: { children: React.ReactNode }) => <>{children}>;
const MockPezkuwiProvider = ({ children }: { children: React.ReactNode }) => <>{children}>;
-const MockLanguageProvider = ({ children }: { children: React.ReactNode }) => <>{children}>;
const MockBiometricAuthProvider = ({ children }: { children: React.ReactNode }) => <>{children}>;
// Wrapper component with all providers
@@ -12,11 +11,9 @@ const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
return (
-
-
- {children}
-
-
+
+ {children}
+
);
diff --git a/mobile/src/components/AvatarPickerModal.tsx b/mobile/src/components/AvatarPickerModal.tsx
index 65aeb68c..ce62080e 100644
--- a/mobile/src/components/AvatarPickerModal.tsx
+++ b/mobile/src/components/AvatarPickerModal.tsx
@@ -16,6 +16,16 @@ import { KurdistanColors } from '../theme/colors';
import { useAuth } from '../contexts/AuthContext';
import { supabase } from '../lib/supabase';
+// Cross-platform alert helper
+const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void}>) => {
+ if (Platform.OS === 'web') {
+ window.alert(`${title}\n\n${message}`);
+ if (buttons?.[0]?.onPress) buttons[0].onPress();
+ } else {
+ showAlert(title, message, buttons);
+ }
+};
+
// Avatar pool - Kurdish/Middle Eastern themed avatars
const AVATAR_POOL = [
{ id: 'avatar1', emoji: '👨🏻', label: 'Man 1' },
@@ -74,7 +84,7 @@ const AvatarPickerModal: React.FC = ({
if (Platform.OS !== 'web') {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
- Alert.alert(
+ showAlert(
'Permission Required',
'Sorry, we need camera roll permissions to upload your photo!'
);
@@ -111,63 +121,88 @@ const AvatarPickerModal: React.FC = ({
if (__DEV__) console.log('[AvatarPicker] Upload successful:', uploadedUrl);
setUploadedImageUri(uploadedUrl);
setSelectedAvatar(null); // Clear emoji selection
- Alert.alert('Success', 'Photo uploaded successfully!');
+ showAlert('Success', 'Photo uploaded successfully!');
} else {
if (__DEV__) console.error('[AvatarPicker] Upload failed: no URL returned');
- Alert.alert('Upload Failed', 'Could not upload your photo. Please check your internet connection and try again.');
+ showAlert('Upload Failed', 'Could not upload your photo. Please check your internet connection and try again.');
}
}
} catch (error) {
setIsUploading(false);
if (__DEV__) console.error('[AvatarPicker] Error picking image:', error);
- Alert.alert('Error', 'Failed to pick image. Please try again.');
+ showAlert('Error', 'Failed to pick image. Please try again.');
}
};
const uploadImageToSupabase = async (imageUri: string): Promise => {
if (!user) {
- if (__DEV__) console.error('[AvatarPicker] No user found');
+ console.error('[AvatarPicker] No user found');
return null;
}
try {
- if (__DEV__) console.log('[AvatarPicker] Fetching image blob...');
- // Convert image URI to blob for web, or use file for native
+ console.log('[AvatarPicker] Starting upload for URI:', imageUri.substring(0, 50) + '...');
+
+ // Convert image URI to blob
const response = await fetch(imageUri);
const blob = await response.blob();
- if (__DEV__) console.log('[AvatarPicker] Blob size:', blob.size, 'bytes');
+ console.log('[AvatarPicker] Blob created - size:', blob.size, 'bytes, type:', blob.type);
- // Generate unique filename
- const fileExt = imageUri.split('.').pop()?.toLowerCase() || 'jpg';
- const fileName = `${user.id}-${Date.now()}.${fileExt}`;
- const filePath = `avatars/${fileName}`;
- if (__DEV__) console.log('[AvatarPicker] Uploading to:', filePath);
-
- // Upload to Supabase Storage
- const { data: uploadData, error: uploadError } = await supabase.storage
- .from('profiles')
- .upload(filePath, blob, {
- contentType: `image/${fileExt}`,
- upsert: false,
- });
-
- if (uploadError) {
- if (__DEV__) console.error('[AvatarPicker] Upload error:', uploadError);
+ if (blob.size === 0) {
+ console.error('[AvatarPicker] Blob is empty!');
return null;
}
- if (__DEV__) console.log('[AvatarPicker] Upload successful:', uploadData);
+ // Get file extension from blob type or URI
+ let fileExt = 'jpg';
+ if (blob.type) {
+ // Extract extension from MIME type (e.g., 'image/jpeg' -> 'jpeg')
+ const mimeExt = blob.type.split('/')[1];
+ if (mimeExt && mimeExt !== 'octet-stream') {
+ fileExt = mimeExt === 'jpeg' ? 'jpg' : mimeExt;
+ }
+ } else if (!imageUri.startsWith('blob:') && !imageUri.startsWith('data:')) {
+ // Try to get extension from URI for non-blob URIs
+ const uriExt = imageUri.split('.').pop()?.toLowerCase();
+ if (uriExt && ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(uriExt)) {
+ fileExt = uriExt;
+ }
+ }
+
+ const fileName = `${user.id}-${Date.now()}.${fileExt}`;
+ const filePath = `avatars/${fileName}`;
+ const contentType = blob.type || `image/${fileExt}`;
+
+ console.log('[AvatarPicker] Uploading to path:', filePath, 'contentType:', contentType);
+
+ // Upload to Supabase Storage
+ const { data: uploadData, error: uploadError } = await supabase.storage
+ .from('avatars')
+ .upload(filePath, blob, {
+ contentType: contentType,
+ upsert: true, // Allow overwriting if file exists
+ });
+
+ if (uploadError) {
+ console.error('[AvatarPicker] Supabase upload error:', uploadError.message, uploadError);
+ // Show more specific error to user
+ showAlert('Upload Error', `Storage error: ${uploadError.message}`);
+ return null;
+ }
+
+ console.log('[AvatarPicker] Upload successful:', uploadData);
// Get public URL
const { data } = supabase.storage
- .from('profiles')
+ .from('avatars')
.getPublicUrl(filePath);
- if (__DEV__) console.log('[AvatarPicker] Public URL:', data.publicUrl);
+ console.log('[AvatarPicker] Public URL:', data.publicUrl);
return data.publicUrl;
- } catch (error) {
- if (__DEV__) console.error('[AvatarPicker] Error uploading to Supabase:', error);
+ } catch (error: any) {
+ console.error('[AvatarPicker] Error uploading to Supabase:', error?.message || error);
+ showAlert('Upload Error', `Failed to upload: ${error?.message || 'Unknown error'}`);
return null;
}
};
@@ -176,7 +211,7 @@ const AvatarPickerModal: React.FC = ({
const avatarToSave = uploadedImageUri || selectedAvatar;
if (!avatarToSave || !user) {
- Alert.alert('Error', 'Please select an avatar or upload a photo');
+ showAlert('Error', 'Please select an avatar or upload a photo');
return;
}
@@ -199,7 +234,7 @@ const AvatarPickerModal: React.FC = ({
if (__DEV__) console.log('[AvatarPicker] Avatar saved successfully:', data);
- Alert.alert('Success', 'Avatar updated successfully!');
+ showAlert('Success', 'Avatar updated successfully!');
if (onAvatarSelected) {
onAvatarSelected(avatarToSave);
@@ -208,7 +243,7 @@ const AvatarPickerModal: React.FC = ({
onClose();
} catch (error) {
if (__DEV__) console.error('[AvatarPicker] Error updating avatar:', error);
- Alert.alert('Error', 'Failed to update avatar. Please try again.');
+ showAlert('Error', 'Failed to update avatar. Please try again.');
} finally {
setIsSaving(false);
}
diff --git a/mobile/src/components/KurdistanSun.tsx b/mobile/src/components/KurdistanSun.tsx
new file mode 100644
index 00000000..c504f924
--- /dev/null
+++ b/mobile/src/components/KurdistanSun.tsx
@@ -0,0 +1,224 @@
+import React, { useEffect, useRef } from 'react';
+import { View, Animated, Easing, StyleSheet } from 'react-native';
+import Svg, { Circle, Line, Defs, RadialGradient, Stop } from 'react-native-svg';
+
+interface KurdistanSunProps {
+ size?: number;
+}
+
+const AnimatedView = Animated.View;
+
+export const KurdistanSun: React.FC = ({ size = 200 }) => {
+ // Animation values
+ const greenHaloRotation = useRef(new Animated.Value(0)).current;
+ const redHaloRotation = useRef(new Animated.Value(0)).current;
+ const yellowHaloRotation = useRef(new Animated.Value(0)).current;
+ const raysPulse = useRef(new Animated.Value(1)).current;
+ const glowPulse = useRef(new Animated.Value(0.6)).current;
+
+ useEffect(() => {
+ // Green halo rotation (3s, clockwise)
+ Animated.loop(
+ Animated.timing(greenHaloRotation, {
+ toValue: 1,
+ duration: 3000,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ })
+ ).start();
+
+ // Red halo rotation (2.5s, counter-clockwise)
+ Animated.loop(
+ Animated.timing(redHaloRotation, {
+ toValue: -1,
+ duration: 2500,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ })
+ ).start();
+
+ // Yellow halo rotation (2s, clockwise)
+ Animated.loop(
+ Animated.timing(yellowHaloRotation, {
+ toValue: 1,
+ duration: 2000,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ })
+ ).start();
+
+ // Rays pulse animation
+ Animated.loop(
+ Animated.sequence([
+ Animated.timing(raysPulse, {
+ toValue: 0.7,
+ duration: 1000,
+ easing: Easing.inOut(Easing.ease),
+ useNativeDriver: true,
+ }),
+ Animated.timing(raysPulse, {
+ toValue: 1,
+ duration: 1000,
+ easing: Easing.inOut(Easing.ease),
+ useNativeDriver: true,
+ }),
+ ])
+ ).start();
+
+ // Glow pulse animation
+ Animated.loop(
+ Animated.sequence([
+ Animated.timing(glowPulse, {
+ toValue: 0.3,
+ duration: 1000,
+ easing: Easing.inOut(Easing.ease),
+ useNativeDriver: true,
+ }),
+ Animated.timing(glowPulse, {
+ toValue: 0.6,
+ duration: 1000,
+ easing: Easing.inOut(Easing.ease),
+ useNativeDriver: true,
+ }),
+ ])
+ ).start();
+ }, []);
+
+ const greenSpin = greenHaloRotation.interpolate({
+ inputRange: [0, 1],
+ outputRange: ['0deg', '360deg'],
+ });
+
+ const redSpin = redHaloRotation.interpolate({
+ inputRange: [-1, 0],
+ outputRange: ['-360deg', '0deg'],
+ });
+
+ const yellowSpin = yellowHaloRotation.interpolate({
+ inputRange: [0, 1],
+ outputRange: ['0deg', '360deg'],
+ });
+
+ const haloSize = size * 0.9;
+ const borderWidth = size * 0.02;
+
+ // Generate 21 rays for Kurdistan flag
+ const rays = Array.from({ length: 21 }).map((_, i) => {
+ const angle = (i * 360) / 21;
+ return (
+
+ );
+ });
+
+ return (
+
+ {/* Rotating colored halos */}
+
+ {/* Green halo (outermost) */}
+
+ {/* Red halo (middle) */}
+
+ {/* Yellow halo (inner) */}
+
+
+
+ {/* Kurdistan Sun SVG with 21 rays */}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'relative',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ halosContainer: {
+ position: 'absolute',
+ width: '100%',
+ height: '100%',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ halo: {
+ position: 'absolute',
+ borderRadius: 1000,
+ },
+ svgContainer: {
+ position: 'relative',
+ zIndex: 1,
+ },
+});
+
+export default KurdistanSun;
diff --git a/mobile/src/components/icons/HezTokenLogo.tsx b/mobile/src/components/icons/HezTokenLogo.tsx
new file mode 100644
index 00000000..dde00a7c
--- /dev/null
+++ b/mobile/src/components/icons/HezTokenLogo.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import Svg, { Path } from 'react-native-svg';
+
+interface HezTokenLogoProps {
+ size?: number;
+ color?: string;
+}
+
+const HezTokenLogo: React.FC = ({ size = 56, color = '#008F43' }) => {
+ return (
+
+ );
+};
+
+export default HezTokenLogo;
diff --git a/mobile/src/components/icons/PezTokenLogo.tsx b/mobile/src/components/icons/PezTokenLogo.tsx
new file mode 100644
index 00000000..c730626a
--- /dev/null
+++ b/mobile/src/components/icons/PezTokenLogo.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import Svg, { Path } from 'react-native-svg';
+
+interface PezTokenLogoProps {
+ size?: number;
+ color?: string;
+}
+
+const PezTokenLogo: React.FC = ({ size = 56, color = '#E91E8C' }) => {
+ return (
+
+ );
+};
+
+export default PezTokenLogo;
diff --git a/mobile/src/components/icons/index.ts b/mobile/src/components/icons/index.ts
new file mode 100644
index 00000000..21e633d6
--- /dev/null
+++ b/mobile/src/components/icons/index.ts
@@ -0,0 +1,2 @@
+export { default as HezTokenLogo } from './HezTokenLogo';
+export { default as PezTokenLogo } from './PezTokenLogo';
diff --git a/mobile/src/contexts/LanguageContext.tsx b/mobile/src/contexts/LanguageContext.tsx
deleted file mode 100644
index 1a7b67ec..00000000
--- a/mobile/src/contexts/LanguageContext.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
-import { I18nManager } from 'react-native';
-import { isRTL, languages } from '../i18n';
-import i18n from '../i18n';
-
-// Language is set at build time via environment variable
-const BUILD_LANGUAGE = process.env.EXPO_PUBLIC_DEFAULT_LANGUAGE || 'en';
-
-interface Language {
- code: string;
- name: string;
- nativeName: string;
- rtl: boolean;
-}
-
-interface LanguageContextType {
- currentLanguage: string;
- isRTL: boolean;
- hasSelectedLanguage: boolean;
- availableLanguages: Language[];
-}
-
-const LanguageContext = createContext(undefined);
-
-export const LanguageProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
- // Language is fixed at build time - no runtime switching
- const [currentLanguage] = useState(BUILD_LANGUAGE);
- const [currentIsRTL] = useState(isRTL(BUILD_LANGUAGE));
-
- useEffect(() => {
- // Initialize i18n with build-time language
- i18n.changeLanguage(BUILD_LANGUAGE);
-
- // Set RTL if needed
- const isRTLLanguage = ['ar', 'ckb', 'fa'].includes(BUILD_LANGUAGE);
- I18nManager.allowRTL(isRTLLanguage);
- I18nManager.forceRTL(isRTLLanguage);
-
- if (__DEV__) {
- console.log(`[LanguageContext] Build language: ${BUILD_LANGUAGE}, RTL: ${isRTLLanguage}`);
- }
- }, []);
-
- return (
-
- {children}
-
- );
-};
-
-export const useLanguage = (): LanguageContextType => {
- const context = useContext(LanguageContext);
- if (!context) {
- throw new Error('useLanguage must be used within LanguageProvider');
- }
- return context;
-};
diff --git a/mobile/src/contexts/PezkuwiContext.tsx b/mobile/src/contexts/PezkuwiContext.tsx
index b26bf8d8..1e6ec746 100644
--- a/mobile/src/contexts/PezkuwiContext.tsx
+++ b/mobile/src/contexts/PezkuwiContext.tsx
@@ -1,4 +1,5 @@
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
+import { Platform } from 'react-native';
import { Keyring } from '@pezkuwi/keyring';
import { KeyringPair } from '@pezkuwi/keyring/types';
import { ApiPromise, WsProvider } from '@pezkuwi/api';
@@ -7,6 +8,34 @@ import * as SecureStore from 'expo-secure-store';
import { cryptoWaitReady, mnemonicGenerate } from '@pezkuwi/util-crypto';
import { ENV } from '../config/environment';
+// Secure storage helper - uses SecureStore on native, AsyncStorage on web (with warning)
+const secureStorage = {
+ setItem: async (key: string, value: string): Promise => {
+ if (Platform.OS === 'web') {
+ // WARNING: AsyncStorage is NOT secure for storing seeds on web
+ // In production, consider using Web Crypto API or server-side storage
+ if (__DEV__) console.warn('[SecureStorage] Using AsyncStorage on web - NOT SECURE for production');
+ await AsyncStorage.setItem(key, value);
+ } else {
+ await SecureStore.setItemAsync(key, value);
+ }
+ },
+ getItem: async (key: string): Promise => {
+ if (Platform.OS === 'web') {
+ return await AsyncStorage.getItem(key);
+ } else {
+ return await SecureStore.getItemAsync(key);
+ }
+ },
+ removeItem: async (key: string): Promise => {
+ if (Platform.OS === 'web') {
+ await AsyncStorage.removeItem(key);
+ } else {
+ await SecureStore.deleteItemAsync(key);
+ }
+ },
+};
+
interface Account {
address: string;
name: string;
@@ -15,14 +44,14 @@ interface Account {
};
}
-export type NetworkType = 'pezkuwi' | 'dicle' | 'zagros' | 'bizinikiwi';
+export type NetworkType = 'pezkuwi' | 'dicle' | 'zagros' | 'bizinikiwi' | 'zombienet';
export interface NetworkConfig {
name: string;
displayName: string;
rpcEndpoint: string;
ss58Format: number;
- type: 'mainnet' | 'testnet' | 'canary';
+ type: 'mainnet' | 'testnet' | 'canary' | 'dev';
}
export const NETWORKS: Record = {
@@ -54,6 +83,13 @@ export const NETWORKS: Record = {
ss58Format: 42,
type: 'testnet',
},
+ zombienet: {
+ name: 'zombienet',
+ displayName: 'Zombienet Dev (Alice/Bob)',
+ rpcEndpoint: 'wss://zombienet-rpc.pezkuwichain.io',
+ ss58Format: 42,
+ type: 'dev',
+ },
};
interface PezkuwiContextType {
@@ -73,6 +109,7 @@ interface PezkuwiContextType {
disconnectWallet: () => void;
createWallet: (name: string, mnemonic?: string) => Promise<{ address: string; mnemonic: string }>;
importWallet: (name: string, mnemonic: string) => Promise<{ address: string }>;
+ deleteWallet: (address: string) => Promise;
getKeyPair: (address: string) => Promise;
signMessage: (address: string, message: string) => Promise;
error: string | null;
@@ -131,7 +168,14 @@ export const PezkuwiProvider: React.FC = ({ children }) =>
const provider = new WsProvider(networkConfig.rpcEndpoint);
console.log('📡 [Pezkuwi] WsProvider created, creating API...');
const newApi = await ApiPromise.create({ provider });
- console.log('✅ [Pezkuwi] API created successfully');
+
+ // Set SS58 format for address encoding/decoding
+ newApi.registry.setChainProperties(
+ newApi.registry.createType('ChainProperties', {
+ ss58Format: networkConfig.ss58Format,
+ })
+ );
+ console.log(`✅ [Pezkuwi] API created with SS58 format: ${networkConfig.ss58Format}`);
if (isSubscribed) {
setApi(newApi);
@@ -256,9 +300,9 @@ export const PezkuwiProvider: React.FC = ({ children }) =>
setAccounts(updatedAccounts);
await AsyncStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(updatedAccounts));
- // SECURITY: Store encrypted seed in SecureStore (hardware-backed storage)
+ // SECURITY: Store encrypted seed in secure storage (hardware-backed on native)
const seedKey = `pezkuwi_seed_${pair.address}`;
- await SecureStore.setItemAsync(seedKey, mnemonicPhrase);
+ await secureStorage.setItem(seedKey, mnemonicPhrase);
if (__DEV__) console.log('[Pezkuwi] Wallet created:', pair.address);
@@ -266,24 +310,33 @@ export const PezkuwiProvider: React.FC = ({ children }) =>
address: pair.address,
mnemonic: mnemonicPhrase,
};
- } catch (err) {
- if (__DEV__) console.error('[Pezkuwi] Failed to create wallet:', err);
- throw new Error('Failed to create wallet');
+ } catch (err: any) {
+ if (__DEV__) {
+ console.error('[Pezkuwi] Failed to create wallet:', err);
+ console.error('[Pezkuwi] Error message:', err?.message);
+ console.error('[Pezkuwi] Error stack:', err?.stack);
+ }
+ throw new Error(err?.message || 'Failed to create wallet');
}
};
- // Import existing wallet from mnemonic
+ // Import existing wallet from mnemonic or dev URI (like //Alice)
const importWallet = async (
name: string,
- mnemonic: string
+ seedOrUri: string
): Promise<{ address: string }> => {
if (!keyring) {
throw new Error('Keyring not initialized');
}
try {
- // Create account from mnemonic
- const pair = keyring.addFromMnemonic(mnemonic.trim(), { name });
+ const trimmedInput = seedOrUri.trim();
+ const isDevUri = trimmedInput.startsWith('//');
+
+ // Create account from URI or mnemonic
+ const pair = isDevUri
+ ? keyring.addFromUri(trimmedInput, { name })
+ : keyring.addFromMnemonic(trimmedInput, { name });
// Check if account already exists
if (accounts.some(a => a.address === pair.address)) {
@@ -301,16 +354,49 @@ export const PezkuwiProvider: React.FC = ({ children }) =>
setAccounts(updatedAccounts);
await AsyncStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(updatedAccounts));
- // Store seed securely
+ // Store seed/URI securely
const seedKey = `pezkuwi_seed_${pair.address}`;
- await SecureStore.setItemAsync(seedKey, mnemonic.trim());
+ await secureStorage.setItem(seedKey, trimmedInput);
- if (__DEV__) console.log('[Pezkuwi] Wallet imported:', pair.address);
+ if (__DEV__) console.log('[Pezkuwi] Wallet imported:', pair.address, isDevUri ? '(dev URI)' : '(mnemonic)');
return { address: pair.address };
- } catch (err) {
- if (__DEV__) console.error('[Pezkuwi] Failed to import wallet:', err);
- throw err;
+ } catch (err: any) {
+ if (__DEV__) {
+ console.error('[Pezkuwi] Failed to import wallet:', err);
+ console.error('[Pezkuwi] Error message:', err?.message);
+ }
+ throw new Error(err?.message || 'Failed to import wallet');
+ }
+ };
+
+ // Delete a wallet
+ const deleteWallet = async (address: string): Promise => {
+ try {
+ // Remove from accounts list
+ const updatedAccounts = accounts.filter(a => a.address !== address);
+ setAccounts(updatedAccounts);
+ await AsyncStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(updatedAccounts));
+
+ // Remove seed from secure storage
+ const seedKey = `pezkuwi_seed_${address}`;
+ await secureStorage.removeItem(seedKey);
+
+ // If deleted account was selected, select another one
+ if (selectedAccount?.address === address) {
+ if (updatedAccounts.length > 0) {
+ setSelectedAccount(updatedAccounts[0]);
+ await AsyncStorage.setItem(SELECTED_ACCOUNT_KEY, updatedAccounts[0].address);
+ } else {
+ setSelectedAccount(null);
+ await AsyncStorage.removeItem(SELECTED_ACCOUNT_KEY);
+ }
+ }
+
+ if (__DEV__) console.log('[Pezkuwi] Wallet deleted:', address);
+ } catch (err: any) {
+ if (__DEV__) console.error('[Pezkuwi] Failed to delete wallet:', err);
+ throw new Error(err?.message || 'Failed to delete wallet');
}
};
@@ -321,17 +407,21 @@ export const PezkuwiProvider: React.FC = ({ children }) =>
}
try {
- // SECURITY: Load seed from SecureStore (encrypted storage)
+ // SECURITY: Load seed/URI from secure storage (encrypted on native)
const seedKey = `pezkuwi_seed_${address}`;
- const mnemonic = await SecureStore.getItemAsync(seedKey);
+ const seedOrUri = await secureStorage.getItem(seedKey);
- if (!mnemonic) {
+ if (!seedOrUri) {
if (__DEV__) console.error('[Pezkuwi] No seed found for address:', address);
return null;
}
- // Recreate keypair from mnemonic
- const pair = keyring.addFromMnemonic(mnemonic);
+ // Recreate keypair from URI or mnemonic
+ const isDevUri = seedOrUri.startsWith('//');
+ const pair = isDevUri
+ ? keyring.addFromUri(seedOrUri)
+ : keyring.addFromMnemonic(seedOrUri);
+
return pair;
} catch (err) {
if (__DEV__) console.error('[Pezkuwi] Failed to get keypair:', err);
@@ -431,6 +521,7 @@ export const PezkuwiProvider: React.FC = ({ children }) =>
disconnectWallet,
createWallet,
importWallet,
+ deleteWallet,
getKeyPair,
signMessage,
error,
diff --git a/mobile/src/contexts/__tests__/LanguageContext.test.tsx b/mobile/src/contexts/__tests__/LanguageContext.test.tsx
deleted file mode 100644
index f2511d8a..00000000
--- a/mobile/src/contexts/__tests__/LanguageContext.test.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React from 'react';
-import { renderHook, act } from '@testing-library/react-native';
-import { LanguageProvider, useLanguage } from '../LanguageContext';
-
-// Mock the i18n module relative to src/
-jest.mock('../../i18n', () => ({
- saveLanguage: jest.fn(() => Promise.resolve()),
- getCurrentLanguage: jest.fn(() => 'en'),
- isRTL: jest.fn((code?: string) => {
- const testCode = code || 'en';
- return ['ckb', 'ar', 'fa'].includes(testCode);
- }),
- LANGUAGE_KEY: '@language',
- languages: [
- { code: 'en', name: 'English', nativeName: 'English', rtl: false },
- { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', rtl: false },
- { code: 'kmr', name: 'Kurdish Kurmanji', nativeName: 'Kurmancî', rtl: false },
- { code: 'ckb', name: 'Kurdish Sorani', nativeName: 'سۆرانی', rtl: true },
- { code: 'ar', name: 'Arabic', nativeName: 'العربية', rtl: true },
- { code: 'fa', name: 'Persian', nativeName: 'فارسی', rtl: true },
- ],
-}));
-
-// Wrapper for provider
-const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
-);
-
-describe('LanguageContext', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should provide language context', () => {
- const { result } = renderHook(() => useLanguage(), { wrapper });
-
- expect(result.current).toBeDefined();
- expect(result.current.currentLanguage).toBe('en');
- });
-
- it('should change language', async () => {
- const { result } = renderHook(() => useLanguage(), { wrapper });
-
- await act(async () => {
- await result.current.changeLanguage('kmr');
- });
-
- expect(result.current.currentLanguage).toBe('kmr');
- });
-
- it('should provide available languages', () => {
- const { result } = renderHook(() => useLanguage(), { wrapper });
-
- expect(result.current.availableLanguages).toBeDefined();
- expect(Array.isArray(result.current.availableLanguages)).toBe(true);
- expect(result.current.availableLanguages.length).toBeGreaterThan(0);
- });
-
- it('should handle RTL languages', async () => {
- const { result } = renderHook(() => useLanguage(), { wrapper });
-
- await act(async () => {
- await result.current.changeLanguage('ar');
- });
-
- expect(result.current.isRTL).toBe(true);
- });
-
- it('should handle LTR languages', async () => {
- const { result } = renderHook(() => useLanguage(), { wrapper });
-
- expect(result.current.isRTL).toBe(false);
- });
-
- it('should throw error when used outside provider', () => {
- const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
-
- expect(() => {
- renderHook(() => useLanguage());
- }).toThrow('useLanguage must be used within LanguageProvider');
-
- spy.mockRestore();
- });
-
- it('should handle language change errors gracefully', async () => {
- const { result } = renderHook(() => useLanguage(), { wrapper });
-
- // changeLanguage should not throw but handle errors internally
- await act(async () => {
- await result.current.changeLanguage('en');
- });
-
- expect(result.current.currentLanguage).toBeDefined();
- });
-
- it('should persist language selection', async () => {
- const { result } = renderHook(() => useLanguage(), { wrapper });
-
- await act(async () => {
- await result.current.changeLanguage('tr');
- });
-
- expect(result.current.currentLanguage).toBe('tr');
- });
-});
diff --git a/mobile/src/i18n/__tests__/index.test.ts b/mobile/src/i18n/__tests__/index.test.ts
deleted file mode 100644
index 66b49be6..00000000
--- a/mobile/src/i18n/__tests__/index.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import i18n from '../index';
-
-describe('i18n Configuration', () => {
- it('should be initialized', () => {
- expect(i18n).toBeDefined();
- });
-
- it('should have language property', () => {
- expect(i18n.language).toBeDefined();
- });
-
- it('should have translation function', () => {
- expect(i18n.t).toBeDefined();
- expect(typeof i18n.t).toBe('function');
- });
-
- it('should support changeLanguage', async () => {
- expect(i18n.changeLanguage).toBeDefined();
- await i18n.changeLanguage('en');
- expect(i18n.language).toBe('en');
- });
-});
diff --git a/mobile/src/i18n/index.ts b/mobile/src/i18n/index.ts
deleted file mode 100644
index c7c1c679..00000000
--- a/mobile/src/i18n/index.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import i18n from 'i18next';
-import { initReactI18next } from 'react-i18next';
-
-// Import shared translations and language configurations
-import {
- comprehensiveTranslations as translations,
- LANGUAGES,
- DEFAULT_LANGUAGE,
- isRTL as checkIsRTL,
-} from '../../../shared/i18n';
-
-// Language is set at build time via environment variable
-const BUILD_LANGUAGE = (process.env.EXPO_PUBLIC_DEFAULT_LANGUAGE || DEFAULT_LANGUAGE) as string;
-
-// Available languages (re-export for compatibility)
-export const languages = LANGUAGES;
-
-// Initialize i18n with build-time language only
-const initializeI18n = () => {
- if (__DEV__) {
- console.log(`[i18n] Initializing with build language: ${BUILD_LANGUAGE}`);
- }
-
- i18n
- .use(initReactI18next)
- .init({
- resources: {
- // Only load the build-time language (reduces APK size)
- [BUILD_LANGUAGE]: { translation: translations[BUILD_LANGUAGE as keyof typeof translations] },
- },
- lng: BUILD_LANGUAGE,
- fallbackLng: BUILD_LANGUAGE,
- compatibilityJSON: 'v3',
- interpolation: {
- escapeValue: false,
- },
- });
-
- return BUILD_LANGUAGE;
-};
-
-// Get current language (always returns BUILD_LANGUAGE)
-export const getCurrentLanguage = () => BUILD_LANGUAGE;
-
-// Check if language is RTL
-export const isRTL = (languageCode?: string) => {
- const code = languageCode || BUILD_LANGUAGE;
- return checkIsRTL(code);
-};
-
-// Initialize i18n automatically
-initializeI18n();
-
-export { initializeI18n, BUILD_LANGUAGE };
-export default i18n;
diff --git a/mobile/src/i18n/locales/ar.json b/mobile/src/i18n/locales/ar.json
deleted file mode 100644
index 9a27a32a..00000000
--- a/mobile/src/i18n/locales/ar.json
+++ /dev/null
@@ -1,228 +0,0 @@
-{
- "welcome": {
- "title": "مرحباً بك في بيزكوي",
- "subtitle": "بوابتك للحوكمة اللامركزية",
- "selectLanguage": "اختر لغتك",
- "continue": "متابعة"
- },
- "auth": {
- "signIn": "تسجيل الدخول",
- "signUp": "إنشاء حساب",
- "email": "البريد الإلكتروني",
- "password": "كلمة المرور",
- "confirmPassword": "تأكيد كلمة المرور",
- "forgotPassword": "نسيت كلمة المرور؟",
- "noAccount": "ليس لديك حساب؟",
- "haveAccount": "هل لديك حساب بالفعل؟",
- "createAccount": "إنشاء حساب",
- "welcomeBack": "مرحباً بعودتك!",
- "getStarted": "ابدأ الآن",
- "username": "اسم المستخدم",
- "emailRequired": "البريد الإلكتروني مطلوب",
- "passwordRequired": "كلمة المرور مطلوبة",
- "usernameRequired": "اسم المستخدم مطلوب",
- "signInSuccess": "تم تسجيل الدخول بنجاح!",
- "signUpSuccess": "تم إنشاء الحساب بنجاح!",
- "invalidCredentials": "بريد إلكتروني أو كلمة مرور غير صحيحة",
- "passwordsMustMatch": "يجب أن تتطابق كلمات المرور"
- },
- "dashboard": {
- "title": "لوحة التحكم",
- "wallet": "المحفظة",
- "staking": "التخزين",
- "governance": "الحوكمة",
- "dex": "البورصة",
- "history": "السجل",
- "settings": "الإعدادات",
- "balance": "الرصيد",
- "totalStaked": "إجمالي المخزن",
- "rewards": "المكافآت",
- "activeProposals": "المقترحات النشطة"
- },
- "wallet": {
- "title": "المحفظة",
- "connect": "ربط المحفظة",
- "disconnect": "فصل الاتصال",
- "address": "العنوان",
- "balance": "الرصيد",
- "send": "إرسال",
- "receive": "استقبال",
- "transaction": "المعاملة",
- "history": "السجل"
- },
- "governance": {
- "title": "الحوكمة",
- "vote": "تصويت",
- "voteFor": "تصويت نعم",
- "voteAgainst": "تصويت لا",
- "submitVote": "إرسال التصويت",
- "votingSuccess": "تم تسجيل تصويتك!",
- "selectCandidate": "اختر مرشحاً",
- "multipleSelect": "يمكنك اختيار عدة مرشحين",
- "singleSelect": "اختر مرشحاً واحداً",
- "proposals": "المقترحات",
- "elections": "الانتخابات",
- "parliament": "البرلمان",
- "activeElections": "الانتخابات النشطة",
- "totalVotes": "إجمالي الأصوات",
- "blocksLeft": "الكتل المتبقية",
- "leading": "الرائد"
- },
- "citizenship": {
- "title": "المواطنة",
- "applyForCitizenship": "التقدم للحصول على الجنسية",
- "newCitizen": "مواطن جديد",
- "existingCitizen": "مواطن حالي",
- "fullName": "الاسم الكامل",
- "fatherName": "اسم الأب",
- "motherName": "اسم الأم",
- "tribe": "القبيلة",
- "region": "المنطقة",
- "profession": "المهنة",
- "referralCode": "رمز الإحالة",
- "submitApplication": "إرسال الطلب",
- "applicationSuccess": "تم إرسال طلبك بنجاح!",
- "applicationPending": "طلبك قيد المراجعة",
- "citizenshipBenefits": "مزايا المواطنة",
- "votingRights": "حق التصويت في الحوكمة",
- "exclusiveAccess": "الوصول إلى الخدمات الحصرية",
- "referralRewards": "برنامج مكافآت الإحالة",
- "communityRecognition": "الاعتراف المجتمعي"
- },
- "p2p": {
- "title": "تداول P2P",
- "trade": "تداول",
- "createOffer": "إنشاء عرض",
- "buyToken": "شراء",
- "sellToken": "بيع",
- "amount": "الكمية",
- "price": "السعر",
- "total": "الإجمالي",
- "initiateTrade": "بدء التداول",
- "comingSoon": "قريباً",
- "tradingWith": "التداول مع",
- "available": "متاح",
- "minOrder": "الحد الأدنى للطلب",
- "maxOrder": "الحد الأقصى للطلب",
- "youWillPay": "ستدفع",
- "myOffers": "عروضي",
- "noOffers": "لا توجد عروض",
- "postAd": "نشر إعلان"
- },
- "forum": {
- "title": "المنتدى",
- "categories": "الفئات",
- "threads": "المواضيع",
- "replies": "الردود",
- "views": "المشاهدات",
- "lastActivity": "آخر نشاط",
- "createThread": "إنشاء موضوع",
- "generalDiscussion": "مناقشة عامة",
- "noThreads": "لا توجد مواضيع",
- "pinned": "مثبت",
- "locked": "مقفل"
- },
- "referral": {
- "title": "برنامج الإحالة",
- "myReferralCode": "رمز الإحالة الخاص بي",
- "totalReferrals": "إجمالي الإحالات",
- "activeReferrals": "الإحالات النشطة",
- "totalEarned": "إجمالي الأرباح",
- "pendingRewards": "المكافآت المعلقة",
- "shareCode": "مشاركة الرمز",
- "copyCode": "نسخ الرمز",
- "connectWallet": "ربط المحفظة",
- "inviteFriends": "دعوة الأصدقاء",
- "earnRewards": "احصل على المكافآت",
- "codeCopied": "تم نسخ الرمز!"
- },
- "settings": {
- "title": "الإعدادات",
- "sections": {
- "appearance": "المظهر",
- "language": "اللغة",
- "security": "الأمان",
- "notifications": "الإشعارات",
- "about": "حول"
- },
- "appearance": {
- "darkMode": "الوضع الداكن",
- "darkModeSubtitle": "التبديل بين السمة الفاتحة والداكنة",
- "fontSize": "حجم الخط",
- "fontSizeSubtitle": "الحالي: {{size}}",
- "fontSizePrompt": "اختر حجم الخط المفضل لديك",
- "small": "صغير",
- "medium": "متوسط",
- "large": "كبير"
- },
- "language": {
- "title": "اللغة",
- "changePrompt": "التبديل إلى {{language}}؟",
- "changeSuccess": "تم تحديث اللغة بنجاح!"
- },
- "security": {
- "biometric": "المصادقة البيومترية",
- "biometricSubtitle": "استخدم بصمة الإصبع أو التعرف على الوجه",
- "biometricPrompt": "هل تريد تفعيل المصادقة البيومترية؟",
- "biometricEnabled": "تم تفعيل المصادقة البيومترية",
- "twoFactor": "المصادقة الثنائية",
- "twoFactorSubtitle": "أضف طبقة أمان إضافية",
- "twoFactorPrompt": "المصادقة الثنائية تضيف طبقة أمان إضافية.",
- "twoFactorSetup": "إعداد",
- "changePassword": "تغيير كلمة المرور",
- "changePasswordSubtitle": "تحديث كلمة مرور حسابك"
- },
- "notifications": {
- "push": "الإشعارات الفورية",
- "pushSubtitle": "تلقي التنبيهات والتحديثات",
- "email": "إشعارات البريد الإلكتروني",
- "emailSubtitle": "إدارة تفضيلات البريد الإلكتروني"
- },
- "about": {
- "pezkuwi": "حول بيزكوي",
- "pezkuwiSubtitle": "تعرف أكثر على كردستان الرقمية",
- "pezkuwiMessage": "بيزكوي هو منصة بلوكتشين لامركزية لكردستان الرقمية.\n\nالإصدار: 1.0.0\n\nصُنع بـ ❤️",
- "terms": "شروط الخدمة",
- "privacy": "سياسة الخصوصية",
- "contact": "اتصل بالدعم",
- "contactSubtitle": "احصل على مساعدة من فريقنا",
- "contactEmail": "البريد الإلكتروني: support@pezkuwichain.io"
- },
- "version": {
- "app": "بيزكوي موبايل",
- "number": "الإصدار 1.0.0",
- "copyright": "© 2026 كردستان الرقمية"
- },
- "alerts": {
- "comingSoon": "قريباً",
- "darkModeMessage": "الوضع الداكن سيكون متاحاً قريباً",
- "twoFactorMessage": "إعداد المصادقة الثنائية سيكون متاحاً قريباً",
- "passwordMessage": "تغيير كلمة المرور سيكون متاحاً قريباً",
- "emailMessage": "إعدادات البريد الإلكتروني ستكون متاحة قريباً",
- "termsMessage": "شروط الخدمة ستكون متاحة قريباً",
- "privacyMessage": "سياسة الخصوصية ستكون متاحة قريباً"
- },
- "common": {
- "enable": "تفعيل",
- "cancel": "إلغاء",
- "confirm": "تأكيد",
- "success": "نجح",
- "error": "خطأ"
- }
- },
- "common": {
- "cancel": "إلغاء",
- "confirm": "تأكيد",
- "save": "حفظ",
- "loading": "جاري التحميل...",
- "error": "خطأ",
- "success": "نجاح",
- "retry": "إعادة المحاولة",
- "close": "إغلاق",
- "back": "رجوع",
- "next": "التالي",
- "submit": "إرسال",
- "required": "مطلوب",
- "optional": "اختياري"
- }
-}
\ No newline at end of file
diff --git a/mobile/src/i18n/locales/ckb.json b/mobile/src/i18n/locales/ckb.json
deleted file mode 100644
index 777ddb96..00000000
--- a/mobile/src/i18n/locales/ckb.json
+++ /dev/null
@@ -1,228 +0,0 @@
-{
- "welcome": {
- "title": "بەخێربێیت بۆ پێزکووی",
- "subtitle": "دەرگای تۆ بۆ بەڕێوەبردنی نامەرکەزی",
- "selectLanguage": "زمانەکەت هەڵبژێرە",
- "continue": "بەردەوام بە"
- },
- "auth": {
- "signIn": "چوونەژوورەوە",
- "signUp": "تۆمارکردن",
- "email": "ئیمەیڵ",
- "password": "وشەی نهێنی",
- "username": "ناوی بەکارهێنەر",
- "confirmPassword": "پشتڕاستکردنەوەی وشەی نهێنی",
- "forgotPassword": "وشەی نهێنیت لەبیرکردووە؟",
- "noAccount": "هەژمارت نییە؟",
- "haveAccount": "هەژمارت هەیە؟",
- "createAccount": "دروستکردنی هەژمار",
- "welcomeBack": "بەخێربێیتەوە!",
- "getStarted": "دەست پێبکە",
- "emailRequired": "ئیمەیڵ پێویستە",
- "passwordRequired": "وشەی نهێنی پێویستە",
- "usernameRequired": "ناوی بەکارهێنەر پێویستە",
- "signInSuccess": "بەسەرکەوتووی چوویتە ژوورەوە!",
- "signUpSuccess": "هەژمار بەسەرکەوتووی دروستکرا!",
- "invalidCredentials": "ئیمەیڵ یان وشەی نهێنی هەڵەیە",
- "passwordsMustMatch": "وشەی نهێنییەکان دەبێت وەک یەک بن"
- },
- "dashboard": {
- "title": "سەرەتا",
- "wallet": "جزدان",
- "staking": "ستەیکینگ",
- "governance": "بەڕێوەبردن",
- "dex": "ئاڵوگۆڕ",
- "history": "مێژوو",
- "settings": "ڕێکخستنەکان",
- "balance": "باڵانس",
- "totalStaked": "کۆی گشتی",
- "rewards": "خەڵات",
- "activeProposals": "پێشنیارە چالاکەکان"
- },
- "governance": {
- "title": "بەڕێوەبردن",
- "vote": "دەنگدان",
- "voteFor": "دەنگی بەڵێ",
- "voteAgainst": "دەنگی نەخێر",
- "submitVote": "ناردنی دەنگ",
- "votingSuccess": "دەنگەکەت تۆمارکرا!",
- "selectCandidate": "کاندیدێک هەڵبژێرە",
- "multipleSelect": "دەتوانیت چەند کاندیدێک هەڵبژێریت",
- "singleSelect": "تەنها کاندیدێک هەڵبژێرە",
- "proposals": "پێشنیارەکان",
- "elections": "هەڵبژاردنەکان",
- "parliament": "پەرلەمان",
- "activeElections": "هەڵبژاردنە چالاکەکان",
- "totalVotes": "کۆی دەنگەکان",
- "blocksLeft": "بلۆکی ماوە",
- "leading": "پێشەنگ"
- },
- "citizenship": {
- "title": "هاووڵاتیێتی",
- "applyForCitizenship": "داوای هاووڵاتیێتی بکە",
- "newCitizen": "هاووڵاتی نوێ",
- "existingCitizen": "هاووڵاتی هەیە",
- "fullName": "ناوی تەواو",
- "fatherName": "ناوی باوک",
- "motherName": "ناوی دایک",
- "tribe": "عەشیرە",
- "region": "هەرێم",
- "profession": "پیشە",
- "referralCode": "کۆدی ئاماژەپێدان",
- "submitApplication": "ناردنی داواکاری",
- "applicationSuccess": "داواکاریەکەت بەسەرکەوتووی نێردرا!",
- "applicationPending": "داواکاریەکەت لە ژێر پێداچوونەوەدایە",
- "citizenshipBenefits": "سوودەکانی هاووڵاتیێتی",
- "votingRights": "مافی دەنگدان لە بەڕێوەبردندا",
- "exclusiveAccess": "دەستگەیشتن بە خزمەتگوزارییە تایبەتەکان",
- "referralRewards": "بەرنامەی خەڵاتی ئاماژەپێدان",
- "communityRecognition": "ناسینەوەی کۆمەڵگە"
- },
- "p2p": {
- "title": "بازرگانیی P2P",
- "trade": "بازرگانی",
- "createOffer": "دروستکردنی پێشنیار",
- "buyToken": "کڕین",
- "sellToken": "فرۆشتن",
- "amount": "بڕ",
- "price": "نرخ",
- "total": "کۆ",
- "initiateTrade": "دەستپێکردنی بازرگانی",
- "comingSoon": "بەم زووانە",
- "tradingWith": "بازرگانی لەگەڵ",
- "available": "بەردەستە",
- "minOrder": "کەمترین داواکاری",
- "maxOrder": "زۆرترین داواکاری",
- "youWillPay": "تۆ دەدەیت",
- "myOffers": "پێشنیارەکانم",
- "noOffers": "هیچ پێشنیارێک نییە",
- "postAd": "ڕیکلام بکە"
- },
- "forum": {
- "title": "فۆرەم",
- "categories": "هاوپۆلەکان",
- "threads": "بابەتەکان",
- "replies": "وەڵامەکان",
- "views": "بینینەکان",
- "lastActivity": "دوا چالاکی",
- "createThread": "دروستکردنی بابەت",
- "generalDiscussion": "گفتوگۆی گشتی",
- "noThreads": "هیچ بابەتێک نییە",
- "pinned": "جێگیرکراو",
- "locked": "داخراو"
- },
- "referral": {
- "title": "بەرنامەی ئاماژەپێدان",
- "myReferralCode": "کۆدی ئاماژەپێدانی من",
- "totalReferrals": "کۆی ئاماژەپێدانەکان",
- "activeReferrals": "ئاماژەپێدانە چالاکەکان",
- "totalEarned": "کۆی قازانج",
- "pendingRewards": "خەڵاتە چاوەڕوانکراوەکان",
- "shareCode": "هاوبەشکردنی کۆد",
- "copyCode": "کۆپیکردنی کۆد",
- "connectWallet": "گرێدانی جزدان",
- "inviteFriends": "بانگهێشتکردنی هاوڕێیان",
- "earnRewards": "خەڵات بەدەستبهێنە",
- "codeCopied": "کۆدەکە کۆپیکرا!"
- },
- "wallet": {
- "title": "جزدان",
- "connect": "گرێدانی جزدان",
- "disconnect": "پچڕاندنی گرێدان",
- "address": "ناونیشان",
- "balance": "باڵانس",
- "send": "ناردن",
- "receive": "وەرگرتن",
- "transaction": "مامەڵە",
- "history": "مێژوو"
- },
- "settings": {
- "title": "ڕێکخستنەکان",
- "sections": {
- "appearance": "دەرکەوتن",
- "language": "زمان",
- "security": "ئاسایش",
- "notifications": "ئاگادارییەکان",
- "about": "دەربارە"
- },
- "appearance": {
- "darkMode": "دۆخی تاریک",
- "darkModeSubtitle": "لە نێوان دۆخی ڕووناک و تاریک بگۆڕە",
- "fontSize": "قەبارەی فۆنت",
- "fontSizeSubtitle": "ئێستا: {{size}}",
- "fontSizePrompt": "قەبارەی فۆنتی دڵخوازت هەڵبژێرە",
- "small": "بچووک",
- "medium": "مامناوەند",
- "large": "گەورە"
- },
- "language": {
- "title": "زمان",
- "changePrompt": "بگۆڕدرێت بۆ {{language}}؟",
- "changeSuccess": "زمان بە سەرکەوتوویی نوێکرایەوە!"
- },
- "security": {
- "biometric": "ناسینەوەی بایۆمێتریک",
- "biometricSubtitle": "پەنجە نوێن یان ناسینەوەی ڕوخسار بەکاربهێنە",
- "biometricPrompt": "دەتەوێت ناسینەوەی بایۆمێتریک چالاک بکەیت؟",
- "biometricEnabled": "ناسینەوەی بایۆمێتریک چالاککرا",
- "twoFactor": "ناسینەوەی دوو-هەنگاوی",
- "twoFactorSubtitle": "چینێکی ئاسایشی زیادە زیاد بکە",
- "twoFactorPrompt": "ناسینەوەی دوو-هەنگاوی چینێکی ئاسایشی زیادە زیاد دەکات.",
- "twoFactorSetup": "ڕێکبخە",
- "changePassword": "وشەی نهێنی بگۆڕە",
- "changePasswordSubtitle": "وشەی نهێنی هەژمارەکەت نوێ بکەرەوە"
- },
- "notifications": {
- "push": "ئاگادارییە خێراکان",
- "pushSubtitle": "ئاگاداری و نوێکارییەکان وەربگرە",
- "email": "ئاگادارییەکانی ئیمەیل",
- "emailSubtitle": "هەڵبژاردنەکانی ئیمەیل بەڕێوەببە"
- },
- "about": {
- "pezkuwi": "دەربارەی پێزکووی",
- "pezkuwiSubtitle": "زیاتر دەربارەی کوردستانی دیجیتاڵ بزانە",
- "pezkuwiMessage": "پێزکووی پلاتفۆرمێکی بلۆکچەینی ناناوەندییە بۆ کوردستانی دیجیتاڵ.\n\nوەشان: 1.0.0\n\nبە ❤️ دروستکرا",
- "terms": "مەرجەکانی خزمەتگوزاری",
- "privacy": "سیاسەتی تایبەتمەندی",
- "contact": "پەیوەندی پشتگیری",
- "contactSubtitle": "یارمەتی لە تیمەکەمان وەربگرە",
- "contactEmail": "ئیمەیل: support@pezkuwichain.io"
- },
- "version": {
- "app": "پێزکووی مۆبایل",
- "number": "وەشان 1.0.0",
- "copyright": "© 2026 کوردستانی دیجیتاڵ"
- },
- "alerts": {
- "comingSoon": "بەم زووانە",
- "darkModeMessage": "دۆخی تاریک لە نوێکردنەوەی داهاتوودا بەردەست دەبێت",
- "twoFactorMessage": "ڕێکخستنی 2FA بەم زووانە بەردەست دەبێت",
- "passwordMessage": "گۆڕینی وشەی نهێنی بەم زووانە بەردەست دەبێت",
- "emailMessage": "ڕێکخستنەکانی ئیمەیل بەم زووانە بەردەست دەبن",
- "termsMessage": "مەرجەکانی خزمەتگوزاری بەم زووانە بەردەست دەبن",
- "privacyMessage": "سیاسەتی تایبەتمەندی بەم زووانە بەردەست دەبێت"
- },
- "common": {
- "enable": "چالاککردن",
- "cancel": "هەڵوەشاندنەوە",
- "confirm": "پشتڕاستکردنەوە",
- "success": "سەرکەوتوو",
- "error": "هەڵە"
- }
- },
- "common": {
- "cancel": "هەڵوەشاندنەوە",
- "confirm": "پشتڕاستکردنەوە",
- "save": "پاشەکەوتکردن",
- "loading": "بارکردن...",
- "error": "هەڵە",
- "success": "سەرکەوتوو",
- "retry": "هەوڵ بدەرەوە",
- "close": "داخستن",
- "back": "گەڕانەوە",
- "next": "دواتر",
- "submit": "ناردن",
- "required": "پێویستە",
- "optional": "ئیختیاری"
- }
-}
\ No newline at end of file
diff --git a/mobile/src/i18n/locales/en.json b/mobile/src/i18n/locales/en.json
deleted file mode 100644
index e6dbb09a..00000000
--- a/mobile/src/i18n/locales/en.json
+++ /dev/null
@@ -1,228 +0,0 @@
-{
- "welcome": {
- "title": "Welcome to Pezkuwi",
- "subtitle": "Your gateway to decentralized governance",
- "selectLanguage": "Select Your Language",
- "continue": "Continue"
- },
- "auth": {
- "signIn": "Sign In",
- "signUp": "Sign Up",
- "email": "Email",
- "password": "Password",
- "username": "Username",
- "confirmPassword": "Confirm Password",
- "forgotPassword": "Forgot Password?",
- "noAccount": "Don't have an account?",
- "haveAccount": "Already have an account?",
- "createAccount": "Create Account",
- "welcomeBack": "Welcome Back!",
- "getStarted": "Get Started",
- "emailRequired": "Email is required",
- "passwordRequired": "Password is required",
- "usernameRequired": "Username is required",
- "signInSuccess": "Signed in successfully!",
- "signUpSuccess": "Account created successfully!",
- "invalidCredentials": "Invalid email or password",
- "passwordsMustMatch": "Passwords must match"
- },
- "dashboard": {
- "title": "Dashboard",
- "wallet": "Wallet",
- "staking": "Staking",
- "governance": "Governance",
- "dex": "Exchange",
- "history": "History",
- "settings": "Settings",
- "balance": "Balance",
- "totalStaked": "Total Staked",
- "rewards": "Rewards",
- "activeProposals": "Active Proposals"
- },
- "governance": {
- "title": "Governance",
- "vote": "Vote",
- "voteFor": "Vote FOR",
- "voteAgainst": "Vote AGAINST",
- "submitVote": "Submit Vote",
- "votingSuccess": "Your vote has been recorded!",
- "selectCandidate": "Select Candidate",
- "multipleSelect": "You can select multiple candidates",
- "singleSelect": "Select one candidate",
- "proposals": "Proposals",
- "elections": "Elections",
- "parliament": "Parliament",
- "activeElections": "Active Elections",
- "totalVotes": "Total Votes",
- "blocksLeft": "Blocks Left",
- "leading": "Leading"
- },
- "citizenship": {
- "title": "Citizenship",
- "applyForCitizenship": "Apply for Citizenship",
- "newCitizen": "New Citizen",
- "existingCitizen": "Existing Citizen",
- "fullName": "Full Name",
- "fatherName": "Father's Name",
- "motherName": "Mother's Name",
- "tribe": "Tribe",
- "region": "Region",
- "profession": "Profession",
- "referralCode": "Referral Code",
- "submitApplication": "Submit Application",
- "applicationSuccess": "Application submitted successfully!",
- "applicationPending": "Your application is pending review",
- "citizenshipBenefits": "Citizenship Benefits",
- "votingRights": "Voting rights in governance",
- "exclusiveAccess": "Access to exclusive services",
- "referralRewards": "Referral rewards program",
- "communityRecognition": "Community recognition"
- },
- "p2p": {
- "title": "P2P Trading",
- "trade": "Trade",
- "createOffer": "Create Offer",
- "buyToken": "Buy",
- "sellToken": "Sell",
- "amount": "Amount",
- "price": "Price",
- "total": "Total",
- "initiateTrade": "Initiate Trade",
- "comingSoon": "Coming Soon",
- "tradingWith": "Trading with",
- "available": "Available",
- "minOrder": "Min Order",
- "maxOrder": "Max Order",
- "youWillPay": "You will pay",
- "myOffers": "My Offers",
- "noOffers": "No offers available",
- "postAd": "Post Ad"
- },
- "forum": {
- "title": "Forum",
- "categories": "Categories",
- "threads": "Threads",
- "replies": "Replies",
- "views": "Views",
- "lastActivity": "Last Activity",
- "createThread": "Create Thread",
- "generalDiscussion": "General Discussion",
- "noThreads": "No threads available",
- "pinned": "Pinned",
- "locked": "Locked"
- },
- "referral": {
- "title": "Referral Program",
- "myReferralCode": "My Referral Code",
- "totalReferrals": "Total Referrals",
- "activeReferrals": "Active Referrals",
- "totalEarned": "Total Earned",
- "pendingRewards": "Pending Rewards",
- "shareCode": "Share Code",
- "copyCode": "Copy Code",
- "connectWallet": "Connect Wallet",
- "inviteFriends": "Invite Friends",
- "earnRewards": "Earn Rewards",
- "codeCopied": "Code copied to clipboard!"
- },
- "wallet": {
- "title": "Wallet",
- "connect": "Connect Wallet",
- "disconnect": "Disconnect",
- "address": "Address",
- "balance": "Balance",
- "send": "Send",
- "receive": "Receive",
- "transaction": "Transaction",
- "history": "History"
- },
- "settings": {
- "title": "Settings",
- "sections": {
- "appearance": "APPEARANCE",
- "language": "LANGUAGE",
- "security": "SECURITY",
- "notifications": "NOTIFICATIONS",
- "about": "ABOUT"
- },
- "appearance": {
- "darkMode": "Dark Mode",
- "darkModeSubtitle": "Switch between light and dark theme",
- "fontSize": "Font Size",
- "fontSizeSubtitle": "Current: {{size}}",
- "fontSizePrompt": "Choose your preferred font size",
- "small": "Small",
- "medium": "Medium",
- "large": "Large"
- },
- "language": {
- "title": "Language",
- "changePrompt": "Switch to {{language}}?",
- "changeSuccess": "Language updated successfully!"
- },
- "security": {
- "biometric": "Biometric Authentication",
- "biometricSubtitle": "Use fingerprint or face recognition",
- "biometricPrompt": "Do you want to enable biometric authentication (fingerprint/face recognition)?",
- "biometricEnabled": "Biometric authentication enabled",
- "twoFactor": "Two-Factor Authentication",
- "twoFactorSubtitle": "Add an extra layer of security",
- "twoFactorPrompt": "Two-factor authentication adds an extra layer of security. You will need to set up an authenticator app.",
- "twoFactorSetup": "Set Up",
- "changePassword": "Change Password",
- "changePasswordSubtitle": "Update your account password"
- },
- "notifications": {
- "push": "Push Notifications",
- "pushSubtitle": "Receive alerts and updates",
- "email": "Email Notifications",
- "emailSubtitle": "Manage email preferences"
- },
- "about": {
- "pezkuwi": "About Pezkuwi",
- "pezkuwiSubtitle": "Learn more about Digital Kurdistan",
- "pezkuwiMessage": "Pezkuwi is a decentralized blockchain platform for Digital Kurdistan, enabling citizens to participate in governance, economy, and social life.\n\nVersion: 1.0.0\n\nBuilt with ❤️ by the Digital Kurdistan team",
- "terms": "Terms of Service",
- "privacy": "Privacy Policy",
- "contact": "Contact Support",
- "contactSubtitle": "Get help from our team",
- "contactEmail": "Email: support@pezkuwichain.io"
- },
- "version": {
- "app": "Pezkuwi Mobile",
- "number": "Version 1.0.0",
- "copyright": "© 2026 Digital Kurdistan"
- },
- "alerts": {
- "comingSoon": "Coming Soon",
- "darkModeMessage": "Dark mode will be available in the next update",
- "twoFactorMessage": "2FA setup will be available soon",
- "passwordMessage": "Password change will be available soon",
- "emailMessage": "Email settings will be available soon",
- "termsMessage": "Terms of Service will be available soon",
- "privacyMessage": "Privacy Policy will be available soon"
- },
- "common": {
- "enable": "Enable",
- "cancel": "Cancel",
- "confirm": "Confirm",
- "success": "Success",
- "error": "Error"
- }
- },
- "common": {
- "cancel": "Cancel",
- "confirm": "Confirm",
- "save": "Save",
- "loading": "Loading...",
- "error": "Error",
- "success": "Success",
- "retry": "Retry",
- "close": "Close",
- "back": "Back",
- "next": "Next",
- "submit": "Submit",
- "required": "Required",
- "optional": "Optional"
- }
-}
diff --git a/mobile/src/i18n/locales/fa.json b/mobile/src/i18n/locales/fa.json
deleted file mode 100644
index 55b031e8..00000000
--- a/mobile/src/i18n/locales/fa.json
+++ /dev/null
@@ -1,163 +0,0 @@
-{
- "welcome": {
- "title": "به پێزکووی خوش آمدید",
- "subtitle": "دروازه شما به حکمرانی غیرمتمرکز",
- "selectLanguage": "زبان خود را انتخاب کنید",
- "continue": "ادامه"
- },
- "auth": {
- "signIn": "ورود",
- "signUp": "ثبت نام",
- "email": "ایمیل",
- "password": "رمز عبور",
- "confirmPassword": "تأیید رمز عبور",
- "forgotPassword": "رمز عبور را فراموش کردهاید؟",
- "noAccount": "حساب کاربری ندارید؟",
- "haveAccount": "قبلاً حساب کاربری دارید؟",
- "createAccount": "ایجاد حساب",
- "welcomeBack": "خوش آمدید!",
- "getStarted": "شروع کنید",
- "username": "نام کاربری",
- "emailRequired": "ایمیل الزامی است",
- "passwordRequired": "رمز عبور الزامی است",
- "usernameRequired": "نام کاربری الزامی است",
- "signInSuccess": "با موفقیت وارد شدید!",
- "signUpSuccess": "حساب با موفقیت ایجاد شد!",
- "invalidCredentials": "ایمیل یا رمز عبور نامعتبر",
- "passwordsMustMatch": "رمزهای عبور باید یکسان باشند"
- },
- "dashboard": {
- "title": "داشبورد",
- "wallet": "کیف پول",
- "staking": "سپردهگذاری",
- "governance": "حکمرانی",
- "dex": "صرافی",
- "history": "تاریخچه",
- "settings": "تنظیمات",
- "balance": "موجودی",
- "totalStaked": "کل سپرده",
- "rewards": "پاداشها",
- "activeProposals": "پیشنهادات فعال"
- },
- "wallet": {
- "title": "کیف پول",
- "connect": "اتصال کیف پول",
- "disconnect": "قطع اتصال",
- "address": "آدرس",
- "balance": "موجودی",
- "send": "ارسال",
- "receive": "دریافت",
- "transaction": "تراکنش",
- "history": "تاریخچه"
- },
- "governance": {
- "title": "حکمرانی",
- "vote": "رأی دادن",
- "voteFor": "رأی موافق",
- "voteAgainst": "رأی مخالف",
- "submitVote": "ثبت رأی",
- "votingSuccess": "رأی شما ثبت شد!",
- "selectCandidate": "انتخاب کاندیدا",
- "multipleSelect": "میتوانید چند کاندیدا انتخاب کنید",
- "singleSelect": "یک کاندیدا انتخاب کنید",
- "proposals": "پیشنهادها",
- "elections": "انتخابات",
- "parliament": "پارلمان",
- "activeElections": "انتخابات فعال",
- "totalVotes": "مجموع آرا",
- "blocksLeft": "بلوکهای باقیمانده",
- "leading": "پیشرو"
- },
- "citizenship": {
- "title": "تابعیت",
- "applyForCitizenship": "درخواست تابعیت",
- "newCitizen": "شهروند جدید",
- "existingCitizen": "شهروند موجود",
- "fullName": "نام کامل",
- "fatherName": "نام پدر",
- "motherName": "نام مادر",
- "tribe": "قبیله",
- "region": "منطقه",
- "profession": "شغل",
- "referralCode": "کد معرف",
- "submitApplication": "ارسال درخواست",
- "applicationSuccess": "درخواست شما با موفقیت ارسال شد!",
- "applicationPending": "درخواست شما در حال بررسی است",
- "citizenshipBenefits": "مزایای تابعیت",
- "votingRights": "حق رأی در حکمرانی",
- "exclusiveAccess": "دسترسی به خدمات انحصاری",
- "referralRewards": "برنامه پاداش معرفی",
- "communityRecognition": "شناخت اجتماعی"
- },
- "p2p": {
- "title": "تجارت P2P",
- "trade": "معامله",
- "createOffer": "ایجاد پیشنهاد",
- "buyToken": "خرید",
- "sellToken": "فروش",
- "amount": "مقدار",
- "price": "قیمت",
- "total": "مجموع",
- "initiateTrade": "شروع معامله",
- "comingSoon": "به زودی",
- "tradingWith": "معامله با",
- "available": "موجود",
- "minOrder": "حداقل سفارش",
- "maxOrder": "حداکثر سفارش",
- "youWillPay": "شما پرداخت خواهید کرد",
- "myOffers": "پیشنهادهای من",
- "noOffers": "پیشنهادی موجود نیست",
- "postAd": "ثبت آگهی"
- },
- "forum": {
- "title": "انجمن",
- "categories": "دستهبندیها",
- "threads": "موضوعات",
- "replies": "پاسخها",
- "views": "بازدیدها",
- "lastActivity": "آخرین فعالیت",
- "createThread": "ایجاد موضوع",
- "generalDiscussion": "بحث عمومی",
- "noThreads": "موضوعی موجود نیست",
- "pinned": "پین شده",
- "locked": "قفل شده"
- },
- "referral": {
- "title": "برنامه معرفی",
- "myReferralCode": "کد معرف من",
- "totalReferrals": "مجموع معرفیها",
- "activeReferrals": "معرفیهای فعال",
- "totalEarned": "مجموع درآمد",
- "pendingRewards": "پاداشهای در انتظار",
- "shareCode": "اشتراکگذاری کد",
- "copyCode": "کپی کد",
- "connectWallet": "اتصال کیف پول",
- "inviteFriends": "دعوت از دوستان",
- "earnRewards": "کسب پاداش",
- "codeCopied": "کد کپی شد!"
- },
- "settings": {
- "title": "تنظیمات",
- "language": "زبان",
- "theme": "تم",
- "notifications": "اعلانها",
- "security": "امنیت",
- "about": "درباره",
- "logout": "خروج"
- },
- "common": {
- "cancel": "لغو",
- "confirm": "تأیید",
- "save": "ذخیره",
- "loading": "در حال بارگذاری...",
- "error": "خطا",
- "success": "موفق",
- "retry": "تلاش مجدد",
- "close": "بستن",
- "back": "بازگشت",
- "next": "بعدی",
- "submit": "ارسال",
- "required": "الزامی",
- "optional": "اختیاری"
- }
-}
diff --git a/mobile/src/i18n/locales/kmr.json b/mobile/src/i18n/locales/kmr.json
deleted file mode 100644
index 334e5453..00000000
--- a/mobile/src/i18n/locales/kmr.json
+++ /dev/null
@@ -1,228 +0,0 @@
-{
- "welcome": {
- "title": "Bi xêr hatî Pezkuwî",
- "subtitle": "Deriyê te yê bo rêveberiya desentralîze",
- "selectLanguage": "Zimanê Xwe Hilbijêre",
- "continue": "Bidomîne"
- },
- "auth": {
- "signIn": "Têkeve",
- "signUp": "Tomar bibe",
- "email": "E-posta",
- "password": "Şîfre",
- "username": "Navê Bikarhêner",
- "confirmPassword": "Şîfreyê Bipejirîne",
- "forgotPassword": "Şîfreyê te ji bîr kiriye?",
- "noAccount": "Hesabê te tune ye?",
- "haveAccount": "Jixwe hesabê te heye?",
- "createAccount": "Hesab Biafirîne",
- "welcomeBack": "Dîsa bi xêr hatî!",
- "getStarted": "Dest pê bike",
- "emailRequired": "E-posta hewce ye",
- "passwordRequired": "Şîfre hewce ye",
- "usernameRequired": "Navê bikarhêner hewce ye",
- "signInSuccess": "Bi serfirazî têkeve!",
- "signUpSuccess": "Hesab bi serfirazî hate afirandin!",
- "invalidCredentials": "E-posta an şîfreya nederbasdar",
- "passwordsMustMatch": "Şîfre divê hevdu bigire"
- },
- "dashboard": {
- "title": "Serûpel",
- "wallet": "Berîk",
- "staking": "Staking",
- "governance": "Rêvebir",
- "dex": "Guherîn",
- "history": "Dîrok",
- "settings": "Mîheng",
- "balance": "Bilanço",
- "totalStaked": "Hemû Stake",
- "rewards": "Xelat",
- "activeProposals": "Pêşniyarên Çalak"
- },
- "governance": {
- "title": "Rêvebir",
- "vote": "Deng bide",
- "voteFor": "Dengê ERÊ",
- "voteAgainst": "Dengê NA",
- "submitVote": "Dengê Xwe Bişîne",
- "votingSuccess": "Dengê we hate tomarkirin!",
- "selectCandidate": "Namzed Hilbijêre",
- "multipleSelect": "Hûn dikarin çend namzedan hilbijêrin",
- "singleSelect": "Yek namzed hilbijêre",
- "proposals": "Pêşniyar",
- "elections": "Hilbijartin",
- "parliament": "Parlamenter",
- "activeElections": "Hilbijartinên Çalak",
- "totalVotes": "Giştî Deng",
- "blocksLeft": "Blokên Mayî",
- "leading": "Pêşeng"
- },
- "citizenship": {
- "title": "Hemwelatî",
- "applyForCitizenship": "Ji bo Hemwelatî Serlêdan Bike",
- "newCitizen": "Hemwelatîyê Nû",
- "existingCitizen": "Hemwelatîya Heyî",
- "fullName": "Nav û Paşnav",
- "fatherName": "Navê Bav",
- "motherName": "Navê Dê",
- "tribe": "Eşîret",
- "region": "Herêm",
- "profession": "Pîşe",
- "referralCode": "Koda Referansê",
- "submitApplication": "Serldanê Bişîne",
- "applicationSuccess": "Serlêdan bi serfirazî hate şandin!",
- "applicationPending": "Serlêdana we di bin lêkolînê de ye",
- "citizenshipBenefits": "Faydeyên Hemwelatî",
- "votingRights": "Mafê dengdanê di rêvebiriyê de",
- "exclusiveAccess": "Gihîştina karûbarên taybet",
- "referralRewards": "Bernameya xelatên referansê",
- "communityRecognition": "Naskirina civakê"
- },
- "p2p": {
- "title": "Bazirganiya P2P",
- "trade": "Bazirganî",
- "createOffer": "Pêşniyar Biafirîne",
- "buyToken": "Bikire",
- "sellToken": "Bifiroşe",
- "amount": "Mîqdar",
- "price": "Biha",
- "total": "Giştî",
- "initiateTrade": "Bazirganiyê Destpêbike",
- "comingSoon": "Pir nêzîk",
- "tradingWith": "Bi re bazirganî",
- "available": "Heyî",
- "minOrder": "Daxwaza Kêm",
- "maxOrder": "Daxwaza Zêde",
- "youWillPay": "Hûn dê bidin",
- "myOffers": "Pêşniyarên Min",
- "noOffers": "Pêşniyar tunene",
- "postAd": "Agahî Bide"
- },
- "forum": {
- "title": "Forum",
- "categories": "Kategoriyan",
- "threads": "Mijar",
- "replies": "Bersiv",
- "views": "Nêrîn",
- "lastActivity": "Çalakiya Dawî",
- "createThread": "Mijar Biafirîne",
- "generalDiscussion": "Gotûbêja Giştî",
- "noThreads": "Mijar tunene",
- "pinned": "Girêdayî",
- "locked": "Girtî"
- },
- "referral": {
- "title": "Bernameya Referansê",
- "myReferralCode": "Koda Referansa Min",
- "totalReferrals": "Giştî Referans",
- "activeReferrals": "Referansên Çalak",
- "totalEarned": "Giştî Qezenc",
- "pendingRewards": "Xelatên Li Benda",
- "shareCode": "Kodê Parve Bike",
- "copyCode": "Kodê Kopî Bike",
- "connectWallet": "Berîkê Girêbide",
- "inviteFriends": "Hevalên Xwe Vexwîne",
- "earnRewards": "Xelat Qezenc Bike",
- "codeCopied": "Kod hate kopîkirin!"
- },
- "wallet": {
- "title": "Berîk",
- "connect": "Berîkê Girêde",
- "disconnect": "Girêdanê Rake",
- "address": "Navnîşan",
- "balance": "Bilanço",
- "send": "Bişîne",
- "receive": "Bistîne",
- "transaction": "Ragihandin",
- "history": "Dîrok"
- },
- "settings": {
- "title": "Mîhengên",
- "sections": {
- "appearance": "XUYANÎ",
- "language": "ZIMAN",
- "security": "EWLEHÎ",
- "notifications": "AGAHDARÎ",
- "about": "DER BARÊ"
- },
- "appearance": {
- "darkMode": "Moda Tarî",
- "darkModeSubtitle": "Di navbera moda ronî û tarî de biguherîne",
- "fontSize": "Mezinahiya Nivîsê",
- "fontSizeSubtitle": "Niha: {{size}}",
- "fontSizePrompt": "Mezinahiya nivîsê ya xwe hilbijêre",
- "small": "Piçûk",
- "medium": "Nav",
- "large": "Mezin"
- },
- "language": {
- "title": "Ziman",
- "changePrompt": "Biguherîne bo {{language}}?",
- "changeSuccess": "Ziman bi serkeftî hate nûkirin!"
- },
- "security": {
- "biometric": "Naskirina Bîyometrîk",
- "biometricSubtitle": "Şopa tilî yan naskirina rû bikar bîne",
- "biometricPrompt": "Hûn dixwazin naskirina bîyometrîk çalak bikin?",
- "biometricEnabled": "Naskirina bîyometrîk çalak kirin",
- "twoFactor": "Naskirina Du-Pîlan",
- "twoFactorSubtitle": "Qateka ewlehiyê zêde bikin",
- "twoFactorPrompt": "Naskirina du-pîlan qateka ewlehiyê zêde dike.",
- "twoFactorSetup": "Saz Bike",
- "changePassword": "Şîfreyê Biguherîne",
- "changePasswordSubtitle": "Şîfreya hesabê xwe nû bike"
- },
- "notifications": {
- "push": "Agahdariyên Zû",
- "pushSubtitle": "Hişyarî û nûvekirinên werbigire",
- "email": "Agahdariyên E-nameyê",
- "emailSubtitle": "Vebijarkên e-nameyê birêve bibin"
- },
- "about": {
- "pezkuwi": "Der barê Pezkuwi",
- "pezkuwiSubtitle": "Zêdetir der barê Kurdistana Dîjîtal bizanin",
- "pezkuwiMessage": "Pezkuwi platformek blockchain-ê ya bê-navend e ji bo Kurdistana Dîjîtal.\n\nGuherto: 1.0.0\n\nBi ❤️ hatiye çêkirin",
- "terms": "Mercên Karûbarê",
- "privacy": "Siyaseta Nepenîtiyê",
- "contact": "Têkiliya Piştgiriyê",
- "contactSubtitle": "Ji tîma me alîkarî bistînin",
- "contactEmail": "E-name: support@pezkuwichain.io"
- },
- "version": {
- "app": "Pezkuwi Mobîl",
- "number": "Guherto 1.0.0",
- "copyright": "© 2026 Kurdistana Dîjîtal"
- },
- "alerts": {
- "comingSoon": "Zû tê",
- "darkModeMessage": "Moda tarî di nûvekirina pêş de berdest dibe",
- "twoFactorMessage": "Sazkirina 2FA zû berdest dibe",
- "passwordMessage": "Guherandina şîfreyê zû berdest dibe",
- "emailMessage": "Mîhengên e-nameyê zû berdest dibin",
- "termsMessage": "Mercên karûbarê zû berdest dibin",
- "privacyMessage": "Siyaseta nepenîtiyê zû berdest dibe"
- },
- "common": {
- "enable": "Çalak Bike",
- "cancel": "Betal Bike",
- "confirm": "Pejirandin",
- "success": "Serkeft",
- "error": "Çewtî"
- }
- },
- "common": {
- "cancel": "Betal bike",
- "confirm": "Bipejirîne",
- "save": "Tomar bike",
- "loading": "Tê barkirin...",
- "error": "Çewtî",
- "success": "Serkeftin",
- "retry": "Dîsa biceribîne",
- "close": "Bigire",
- "back": "Paş",
- "next": "Pêş",
- "submit": "Bişîne",
- "required": "Hewce ye",
- "optional": "Bijarte"
- }
-}
\ No newline at end of file
diff --git a/mobile/src/i18n/locales/tr.json b/mobile/src/i18n/locales/tr.json
deleted file mode 100644
index e86b390d..00000000
--- a/mobile/src/i18n/locales/tr.json
+++ /dev/null
@@ -1,228 +0,0 @@
-{
- "welcome": {
- "title": "Pezkuwi'ye Hoş Geldiniz",
- "subtitle": "Merkezi olmayan yönetim kapınız",
- "selectLanguage": "Dilinizi Seçin",
- "continue": "Devam Et"
- },
- "auth": {
- "signIn": "Giriş Yap",
- "signUp": "Kayıt Ol",
- "email": "E-posta",
- "password": "Şifre",
- "username": "Kullanıcı Adı",
- "confirmPassword": "Şifreyi Onayla",
- "forgotPassword": "Şifremi Unuttum",
- "noAccount": "Hesabınız yok mu?",
- "haveAccount": "Zaten hesabınız var mı?",
- "createAccount": "Hesap Oluştur",
- "welcomeBack": "Tekrar Hoş Geldiniz!",
- "getStarted": "Başlayın",
- "emailRequired": "E-posta gereklidir",
- "passwordRequired": "Şifre gereklidir",
- "usernameRequired": "Kullanıcı adı gereklidir",
- "signInSuccess": "Başarıyla giriş yapıldı!",
- "signUpSuccess": "Hesap başarıyla oluşturuldu!",
- "invalidCredentials": "Geçersiz e-posta veya şifre",
- "passwordsMustMatch": "Şifreler eşleşmelidir"
- },
- "dashboard": {
- "title": "Ana Sayfa",
- "wallet": "Cüzdan",
- "staking": "Stake Etme",
- "governance": "Yönetişim",
- "dex": "Borsa",
- "history": "Geçmiş",
- "settings": "Ayarlar",
- "balance": "Bakiye",
- "totalStaked": "Toplam Stake",
- "rewards": "Ödüller",
- "activeProposals": "Aktif Teklifler"
- },
- "governance": {
- "title": "Yönetişim",
- "vote": "Oy Ver",
- "voteFor": "EVET Oyu",
- "voteAgainst": "HAYIR Oyu",
- "submitVote": "Oyu Gönder",
- "votingSuccess": "Oyunuz kaydedildi!",
- "selectCandidate": "Aday Seç",
- "multipleSelect": "Birden fazla aday seçebilirsiniz",
- "singleSelect": "Bir aday seçin",
- "proposals": "Teklifler",
- "elections": "Seçimler",
- "parliament": "Meclis",
- "activeElections": "Aktif Seçimler",
- "totalVotes": "Toplam Oylar",
- "blocksLeft": "Kalan Bloklar",
- "leading": "Önde Giden"
- },
- "citizenship": {
- "title": "Vatandaşlık",
- "applyForCitizenship": "Vatandaşlığa Başvur",
- "newCitizen": "Yeni Vatandaş",
- "existingCitizen": "Mevcut Vatandaş",
- "fullName": "Ad Soyad",
- "fatherName": "Baba Adı",
- "motherName": "Anne Adı",
- "tribe": "Aşiret",
- "region": "Bölge",
- "profession": "Meslek",
- "referralCode": "Referans Kodu",
- "submitApplication": "Başvuruyu Gönder",
- "applicationSuccess": "Başvuru başarıyla gönderildi!",
- "applicationPending": "Başvurunuz inceleniyor",
- "citizenshipBenefits": "Vatandaşlık Avantajları",
- "votingRights": "Yönetişimde oy hakkı",
- "exclusiveAccess": "Özel hizmetlere erişim",
- "referralRewards": "Referans ödül programı",
- "communityRecognition": "Topluluk tanınması"
- },
- "p2p": {
- "title": "P2P Ticaret",
- "trade": "Ticaret",
- "createOffer": "Teklif Oluştur",
- "buyToken": "Al",
- "sellToken": "Sat",
- "amount": "Miktar",
- "price": "Fiyat",
- "total": "Toplam",
- "initiateTrade": "Ticareti Başlat",
- "comingSoon": "Yakında",
- "tradingWith": "İle ticaret",
- "available": "Mevcut",
- "minOrder": "Min Sipariş",
- "maxOrder": "Max Sipariş",
- "youWillPay": "Ödeyeceğiniz",
- "myOffers": "Tekliflerim",
- "noOffers": "Teklif bulunmuyor",
- "postAd": "İlan Ver"
- },
- "forum": {
- "title": "Forum",
- "categories": "Kategoriler",
- "threads": "Konular",
- "replies": "Cevaplar",
- "views": "Görüntüleme",
- "lastActivity": "Son Aktivite",
- "createThread": "Konu Oluştur",
- "generalDiscussion": "Genel Tartışma",
- "noThreads": "Konu bulunmuyor",
- "pinned": "Sabitlenmiş",
- "locked": "Kilitli"
- },
- "referral": {
- "title": "Referans Programı",
- "myReferralCode": "Referans Kodum",
- "totalReferrals": "Toplam Referanslar",
- "activeReferrals": "Aktif Referanslar",
- "totalEarned": "Toplam Kazanç",
- "pendingRewards": "Bekleyen Ödüller",
- "shareCode": "Kodu Paylaş",
- "copyCode": "Kodu Kopyala",
- "connectWallet": "Cüzdan Bağla",
- "inviteFriends": "Arkadaşlarını Davet Et",
- "earnRewards": "Ödül Kazan",
- "codeCopied": "Kod panoya kopyalandı!"
- },
- "wallet": {
- "title": "Cüzdan",
- "connect": "Cüzdan Bağla",
- "disconnect": "Bağlantıyı Kes",
- "address": "Adres",
- "balance": "Bakiye",
- "send": "Gönder",
- "receive": "Al",
- "transaction": "İşlem",
- "history": "Geçmiş"
- },
- "settings": {
- "title": "Ayarlar",
- "sections": {
- "appearance": "GÖRÜNÜM",
- "language": "DİL",
- "security": "GÜVENLİK",
- "notifications": "BİLDİRİMLER",
- "about": "HAKKINDA"
- },
- "appearance": {
- "darkMode": "Karanlık Mod",
- "darkModeSubtitle": "Açık ve karanlık tema arasında geçiş yapın",
- "fontSize": "Yazı Boyutu",
- "fontSizeSubtitle": "Şu anki: {{size}}",
- "fontSizePrompt": "Tercih ettiğiniz yazı boyutunu seçin",
- "small": "Küçük",
- "medium": "Orta",
- "large": "Büyük"
- },
- "language": {
- "title": "Dil",
- "changePrompt": "{{language}} diline geçilsin mi?",
- "changeSuccess": "Dil başarıyla güncellendi!"
- },
- "security": {
- "biometric": "Biyometrik Kimlik Doğrulama",
- "biometricSubtitle": "Parmak izi veya yüz tanıma kullanın",
- "biometricPrompt": "Biyometrik kimlik doğrulamayı (parmak izi/yüz tanıma) etkinleştirmek istiyor musunuz?",
- "biometricEnabled": "Biyometrik kimlik doğrulama etkinleştirildi",
- "twoFactor": "İki Faktörlü Kimlik Doğrulama",
- "twoFactorSubtitle": "Ekstra bir güvenlik katmanı ekleyin",
- "twoFactorPrompt": "İki faktörlü kimlik doğrulama ekstra bir güvenlik katmanı ekler. Bir kimlik doğrulayıcı uygulama kurmanız gerekecek.",
- "twoFactorSetup": "Kur",
- "changePassword": "Şifre Değiştir",
- "changePasswordSubtitle": "Hesap şifrenizi güncelleyin"
- },
- "notifications": {
- "push": "Anlık Bildirimler",
- "pushSubtitle": "Uyarılar ve güncellemeler alın",
- "email": "E-posta Bildirimleri",
- "emailSubtitle": "E-posta tercihlerini yönetin"
- },
- "about": {
- "pezkuwi": "Pezkuwi Hakkında",
- "pezkuwiSubtitle": "Dijital Kürdistan hakkında daha fazla bilgi edinin",
- "pezkuwiMessage": "Pezkuwi, vatandaşların yönetişim, ekonomi ve sosyal yaşama katılımını sağlayan Dijital Kürdistan için merkezi olmayan bir blockchain platformudur.\n\nVersiyon: 1.0.0\n\nDijital Kürdistan ekibi tarafından ❤️ ile yapıldı",
- "terms": "Hizmet Şartları",
- "privacy": "Gizlilik Politikası",
- "contact": "Destek İletişim",
- "contactSubtitle": "Ekibimizden yardım alın",
- "contactEmail": "E-posta: support@pezkuwichain.io"
- },
- "version": {
- "app": "Pezkuwi Mobil",
- "number": "Versiyon 1.0.0",
- "copyright": "© 2026 Dijital Kürdistan"
- },
- "alerts": {
- "comingSoon": "Yakında",
- "darkModeMessage": "Karanlık mod bir sonraki güncellemede kullanılabilir olacak",
- "twoFactorMessage": "2FA kurulumu yakında kullanılabilir olacak",
- "passwordMessage": "Şifre değiştirme yakında kullanılabilir olacak",
- "emailMessage": "E-posta ayarları yakında kullanılabilir olacak",
- "termsMessage": "Hizmet Şartları yakında kullanılabilir olacak",
- "privacyMessage": "Gizlilik Politikası yakında kullanılabilir olacak"
- },
- "common": {
- "enable": "Etkinleştir",
- "cancel": "İptal",
- "confirm": "Onayla",
- "success": "Başarılı",
- "error": "Hata"
- }
- },
- "common": {
- "cancel": "İptal",
- "confirm": "Onayla",
- "save": "Kaydet",
- "loading": "Yükleniyor...",
- "error": "Hata",
- "success": "Başarılı",
- "retry": "Tekrar Dene",
- "close": "Kapat",
- "back": "Geri",
- "next": "İleri",
- "submit": "Gönder",
- "required": "Gerekli",
- "optional": "İsteğe Bağlı"
- }
-}
diff --git a/mobile/src/navigation/AppNavigator.tsx b/mobile/src/navigation/AppNavigator.tsx
index 5cbc4643..8f409bb5 100644
--- a/mobile/src/navigation/AppNavigator.tsx
+++ b/mobile/src/navigation/AppNavigator.tsx
@@ -15,6 +15,10 @@ import SettingsScreen from '../screens/SettingsScreen';
import BeCitizenChoiceScreen from '../screens/BeCitizenChoiceScreen';
import BeCitizenApplyScreen from '../screens/BeCitizenApplyScreen';
import BeCitizenClaimScreen from '../screens/BeCitizenClaimScreen';
+import EditProfileScreen from '../screens/EditProfileScreen';
+import WalletScreen from '../screens/WalletScreen';
+import WalletSetupScreen from '../screens/WalletSetupScreen';
+import SwapScreen from '../screens/SwapScreen';
export type RootStackParamList = {
Welcome: undefined;
@@ -22,6 +26,10 @@ export type RootStackParamList = {
Auth: undefined;
MainApp: undefined;
Settings: undefined;
+ EditProfile: undefined;
+ Wallet: undefined;
+ WalletSetup: undefined;
+ Swap: undefined;
BeCitizenChoice: undefined;
BeCitizenApply: undefined;
BeCitizenClaim: undefined;
@@ -119,6 +127,34 @@ const AppNavigator: React.FC = () => {
headerBackTitle: 'Back',
}}
/>
+
+
+
+
>
)}
diff --git a/mobile/src/screens/AuthScreen.tsx b/mobile/src/screens/AuthScreen.tsx
index 7768d626..def07439 100644
--- a/mobile/src/screens/AuthScreen.tsx
+++ b/mobile/src/screens/AuthScreen.tsx
@@ -14,12 +14,10 @@ import {
Image,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { useAuth } from '../contexts/AuthContext';
import { KurdistanColors } from '../theme/colors';
const AuthScreen: React.FC = () => {
- const { t } = useTranslation();
const { signIn, signUp } = useAuth();
// Tab state
@@ -47,7 +45,7 @@ const AuthScreen: React.FC = () => {
setError('');
if (!loginEmail || !loginPassword) {
- setError(t('auth.fillAllFields', 'Please fill in all fields'));
+ setError('Please fill in all fields');
return;
}
@@ -58,13 +56,13 @@ const AuthScreen: React.FC = () => {
if (signInError) {
if (signInError.message?.includes('Invalid login credentials')) {
- setError(t('auth.invalidCredentials', 'Email or password is incorrect'));
+ setError('Email or password is incorrect');
} else {
- setError(signInError.message || t('auth.loginFailed', 'Login failed'));
+ setError(signInError.message || 'Login failed');
}
}
} catch (err) {
- setError(t('auth.loginFailed', 'Login failed. Please try again.'));
+ setError('Login failed. Please try again.');
if (__DEV__) console.error('Sign in error:', err);
} finally {
setLoading(false);
@@ -75,17 +73,17 @@ const AuthScreen: React.FC = () => {
setError('');
if (!signupName || !signupEmail || !signupPassword || !signupConfirmPassword) {
- setError(t('auth.fillAllFields', 'Please fill in all required fields'));
+ setError('Please fill in all required fields');
return;
}
if (signupPassword !== signupConfirmPassword) {
- setError(t('auth.passwordsDoNotMatch', 'Passwords do not match'));
+ setError('Passwords do not match');
return;
}
if (signupPassword.length < 8) {
- setError(t('auth.passwordTooShort', 'Password must be at least 8 characters'));
+ setError('Password must be at least 8 characters');
return;
}
@@ -100,10 +98,10 @@ const AuthScreen: React.FC = () => {
);
if (signUpError) {
- setError(signUpError.message || t('auth.signupFailed', 'Sign up failed'));
+ setError(signUpError.message || 'Sign up failed');
}
} catch (err) {
- setError(t('auth.signupFailed', 'Sign up failed. Please try again.'));
+ setError('Sign up failed. Please try again.');
if (__DEV__) console.error('Sign up error:', err);
} finally {
setLoading(false);
@@ -144,7 +142,7 @@ const AuthScreen: React.FC = () => {
PezkuwiChain
- {t('login.subtitle', 'Access your governance account')}
+ Access your governance account
@@ -158,7 +156,7 @@ const AuthScreen: React.FC = () => {
}}
>
- {t('login.signin', 'Sign In')}
+ Sign In
{
}}
>
- {t('login.signup', 'Sign Up')}
+ Sign Up
@@ -178,7 +176,7 @@ const AuthScreen: React.FC = () => {
{activeTab === 'signin' && (
- {t('login.email', 'Email')}
+ Email
✉️
{
- {t('login.password', 'Password')}
+ Password
🔒
{
{rememberMe && ✓}
- {t('login.rememberMe', 'Remember me')}
+ Remember me
- {t('login.forgotPassword', 'Forgot password?')}
+ Forgot password?
@@ -251,7 +249,7 @@ const AuthScreen: React.FC = () => {
) : (
- {t('login.signin', 'Sign In')}
+ Sign In
)}
@@ -262,7 +260,7 @@ const AuthScreen: React.FC = () => {
{activeTab === 'signup' && (
- {t('login.fullName', 'Full Name')}
+ Full Name
👤
{
- {t('login.email', 'Email')}
+ Email
✉️
{
- {t('login.password', 'Password')}
+ Password
🔒
{
- {t('login.confirmPassword', 'Confirm Password')}
+ Confirm Password
🔒
{
- {t('login.referralCode', 'Referral Code')}{' '}
+ Referral Code{' '}
- ({t('login.optional', 'Optional')})
+ (Optional)
👥
{
/>
- {t('login.referralDescription', 'If someone referred you, enter their code here')}
+ If someone referred you, enter their code here
@@ -370,7 +368,7 @@ const AuthScreen: React.FC = () => {
) : (
- {t('login.createAccount', 'Create Account')}
+ Create Account
)}
@@ -380,15 +378,15 @@ const AuthScreen: React.FC = () => {
{/* Footer */}
- {t('login.terms', 'By continuing, you agree to our')}{' '}
+ By continuing, you agree to our{' '}
- {t('login.termsOfService', 'Terms of Service')}
+ Terms of Service
- {t('login.and', 'and')}
+ and
- {t('login.privacyPolicy', 'Privacy Policy')}
+ Privacy Policy
diff --git a/mobile/src/screens/BeCitizenApplyScreen.tsx b/mobile/src/screens/BeCitizenApplyScreen.tsx
index f4233aff..3e916160 100644
--- a/mobile/src/screens/BeCitizenApplyScreen.tsx
+++ b/mobile/src/screens/BeCitizenApplyScreen.tsx
@@ -12,7 +12,6 @@ import {
ActivityIndicator,
Modal,
} from 'react-native';
-import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import {
@@ -97,7 +96,6 @@ const CustomPicker: React.FC<{
};
const BeCitizenApplyScreen: React.FC = () => {
- const { t } = useTranslation();
const navigation = useNavigation();
const { api, selectedAccount } = usePezkuwi();
const [isSubmitting, setIsSubmitting] = useState(false);
diff --git a/mobile/src/screens/BeCitizenChoiceScreen.tsx b/mobile/src/screens/BeCitizenChoiceScreen.tsx
index 364c9c6d..f4aef82f 100644
--- a/mobile/src/screens/BeCitizenChoiceScreen.tsx
+++ b/mobile/src/screens/BeCitizenChoiceScreen.tsx
@@ -9,7 +9,6 @@ import {
StatusBar,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
import type { NavigationProp } from '@react-navigation/native';
@@ -21,7 +20,6 @@ type RootStackParamList = {
};
const BeCitizenChoiceScreen: React.FC = () => {
- const { t } = useTranslation();
const navigation = useNavigation>();
return (
diff --git a/mobile/src/screens/BeCitizenClaimScreen.tsx b/mobile/src/screens/BeCitizenClaimScreen.tsx
index 30632e83..ca08b40e 100644
--- a/mobile/src/screens/BeCitizenClaimScreen.tsx
+++ b/mobile/src/screens/BeCitizenClaimScreen.tsx
@@ -10,14 +10,12 @@ import {
Alert,
ActivityIndicator,
} from 'react-native';
-import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { getCitizenshipStatus } from '@pezkuwi/lib/citizenship-workflow';
import { KurdistanColors } from '../theme/colors';
const BeCitizenClaimScreen: React.FC = () => {
- const { t } = useTranslation();
const navigation = useNavigation();
const { api, selectedAccount } = usePezkuwi();
const [isSubmitting, setIsSubmitting] = useState(false);
diff --git a/mobile/src/screens/BeCitizenScreen.tsx b/mobile/src/screens/BeCitizenScreen.tsx
index dce861af..2af94a3f 100644
--- a/mobile/src/screens/BeCitizenScreen.tsx
+++ b/mobile/src/screens/BeCitizenScreen.tsx
@@ -12,7 +12,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import {
submitKycApplication,
@@ -22,7 +21,6 @@ import {
import { KurdistanColors } from '../theme/colors';
const BeCitizenScreen: React.FC = () => {
- const { t: _t } = useTranslation();
const { api, selectedAccount } = usePezkuwi();
const [_isExistingCitizen, _setIsExistingCitizen] = useState(false);
const [currentStep, setCurrentStep] = useState<'choice' | 'new' | 'existing'>('choice');
diff --git a/mobile/src/screens/DashboardScreen.tsx b/mobile/src/screens/DashboardScreen.tsx
index 47353453..4aeb4239 100644
--- a/mobile/src/screens/DashboardScreen.tsx
+++ b/mobile/src/screens/DashboardScreen.tsx
@@ -14,7 +14,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import type { NavigationProp } from '@react-navigation/native';
import type { BottomTabParamList } from '../navigation/BottomTabNavigator';
@@ -82,7 +81,6 @@ const getEmojiFromAvatarId = (avatarId: string): string => {
interface DashboardScreenProps {}
const DashboardScreen: React.FC = () => {
- const { t } = useTranslation();
const navigation = useNavigation>();
const { user } = useAuth();
const { api, isApiReady, selectedAccount } = usePezkuwi();
@@ -171,8 +169,8 @@ const DashboardScreen: React.FC = () => {
const showComingSoon = (featureName: string) => {
Alert.alert(
- t('settingsScreen.comingSoon'),
- `${featureName} ${t('settingsScreen.comingSoonMessage')}`,
+ 'Coming Soon',
+ `${featureName} will be available soon!`,
[{ text: 'OK' }]
);
};
@@ -431,21 +429,8 @@ const DashboardScreen: React.FC = () => {
- {/* Wallet Visitors - Everyone can use */}
- {renderAppIcon('Wallet Visitors', '👁️', () => showComingSoon('Wallet Visitors'), true)}
-
- {/* Wallet Welati - Only Citizens can use */}
- {renderAppIcon('Wallet Welati', '🏛️', () => {
- if (tikis.includes('Citizen') || tikis.includes('Welati')) {
- showComingSoon('Wallet Welati');
- } else {
- Alert.alert(
- 'Citizens Only',
- 'Wallet Welati is only available to Pezkuwi citizens. Please apply for citizenship first.',
- [{ text: 'OK' }]
- );
- }
- }, true, !tikis.includes('Citizen') && !tikis.includes('Welati'))}
+ {/* Wallet - Navigate to WalletScreen */}
+ {renderAppIcon('Wallet', '👛', () => navigation.navigate('Wallet'), true)}
{renderAppIcon('Bank', qaBank, () => showComingSoon('Bank'), false, true)}
{renderAppIcon('Exchange', qaExchange, () => showComingSoon('Swap'), false)}
diff --git a/mobile/src/screens/EditProfileScreen.tsx b/mobile/src/screens/EditProfileScreen.tsx
new file mode 100644
index 00000000..63646d4e
--- /dev/null
+++ b/mobile/src/screens/EditProfileScreen.tsx
@@ -0,0 +1,450 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ StyleSheet,
+ SafeAreaView,
+ ScrollView,
+ TextInput,
+ ActivityIndicator,
+ Alert,
+ Platform,
+ KeyboardAvoidingView,
+} from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { useAuth } from '../contexts/AuthContext';
+import { useTheme } from '../contexts/ThemeContext';
+import { KurdistanColors } from '../theme/colors';
+import { supabase } from '../lib/supabase';
+import AvatarPickerModal from '../components/AvatarPickerModal';
+
+// Cross-platform alert helper
+const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void; style?: string}>) => {
+ if (Platform.OS === 'web') {
+ if (buttons && buttons.length > 1) {
+ const result = window.confirm(`${title}\n\n${message}`);
+ if (result && buttons[1]?.onPress) {
+ buttons[1].onPress();
+ } else if (!result && buttons[0]?.onPress) {
+ buttons[0].onPress();
+ }
+ } else {
+ window.alert(`${title}\n\n${message}`);
+ if (buttons?.[0]?.onPress) buttons[0].onPress();
+ }
+ } else {
+ Alert.alert(title, message, buttons as any);
+ }
+};
+
+// Avatar pool matching AvatarPickerModal
+const AVATAR_POOL = [
+ { id: 'avatar1', emoji: '👨🏻' },
+ { id: 'avatar2', emoji: '👨🏼' },
+ { id: 'avatar3', emoji: '👨🏽' },
+ { id: 'avatar4', emoji: '👨🏾' },
+ { id: 'avatar5', emoji: '👩🏻' },
+ { id: 'avatar6', emoji: '👩🏼' },
+ { id: 'avatar7', emoji: '👩🏽' },
+ { id: 'avatar8', emoji: '👩🏾' },
+ { id: 'avatar9', emoji: '🧔🏻' },
+ { id: 'avatar10', emoji: '🧔🏼' },
+ { id: 'avatar11', emoji: '🧔🏽' },
+ { id: 'avatar12', emoji: '🧔🏾' },
+ { id: 'avatar13', emoji: '👳🏻♂️' },
+ { id: 'avatar14', emoji: '👳🏼♂️' },
+ { id: 'avatar15', emoji: '👳🏽♂️' },
+ { id: 'avatar16', emoji: '🧕🏻' },
+ { id: 'avatar17', emoji: '🧕🏼' },
+ { id: 'avatar18', emoji: '🧕🏽' },
+ { id: 'avatar19', emoji: '👴🏻' },
+ { id: 'avatar20', emoji: '👴🏼' },
+ { id: 'avatar21', emoji: '👵🏻' },
+ { id: 'avatar22', emoji: '👵🏼' },
+ { id: 'avatar23', emoji: '👦🏻' },
+ { id: 'avatar24', emoji: '👦🏼' },
+ { id: 'avatar25', emoji: '👧🏻' },
+ { id: 'avatar26', emoji: '👧🏼' },
+];
+
+const getEmojiFromAvatarId = (avatarId: string): string => {
+ const avatar = AVATAR_POOL.find(a => a.id === avatarId);
+ return avatar ? avatar.emoji : '👤';
+};
+
+const EditProfileScreen: React.FC = () => {
+ const navigation = useNavigation();
+ const { user } = useAuth();
+ const { isDarkMode, colors, fontScale } = useTheme();
+
+ const [fullName, setFullName] = useState('');
+ const [avatarUrl, setAvatarUrl] = useState(null);
+ const [originalName, setOriginalName] = useState('');
+ const [originalAvatar, setOriginalAvatar] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [avatarModalVisible, setAvatarModalVisible] = useState(false);
+
+ useEffect(() => {
+ fetchProfile();
+ }, []);
+
+ const fetchProfile = async () => {
+ if (!user) {
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const { data, error } = await supabase
+ .from('profiles')
+ .select('full_name, avatar_url')
+ .eq('id', user.id)
+ .single();
+
+ if (error) throw error;
+
+ setFullName(data?.full_name || '');
+ setAvatarUrl(data?.avatar_url || null);
+ setOriginalName(data?.full_name || '');
+ setOriginalAvatar(data?.avatar_url || null);
+ } catch (error) {
+ if (__DEV__) console.error('Error fetching profile:', error);
+ showAlert('Error', 'Failed to load profile data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const hasChanges = () => {
+ return fullName !== originalName || avatarUrl !== originalAvatar;
+ };
+
+ const handleSave = async () => {
+ if (!user) return;
+
+ if (!hasChanges()) {
+ navigation.goBack();
+ return;
+ }
+
+ setSaving(true);
+ try {
+ const updates: { full_name?: string | null; avatar_url?: string | null } = {};
+
+ if (fullName !== originalName) {
+ updates.full_name = fullName.trim() || null;
+ }
+ if (avatarUrl !== originalAvatar) {
+ updates.avatar_url = avatarUrl;
+ }
+
+ const { error } = await supabase
+ .from('profiles')
+ .update(updates)
+ .eq('id', user.id);
+
+ if (error) throw error;
+
+ showAlert('Success', 'Profile updated successfully', [
+ { text: 'OK', onPress: () => navigation.goBack() }
+ ]);
+ } catch (error) {
+ if (__DEV__) console.error('Error saving profile:', error);
+ showAlert('Error', 'Failed to save profile. Please try again.');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (hasChanges()) {
+ showAlert(
+ 'Discard Changes?',
+ 'You have unsaved changes. Are you sure you want to go back?',
+ [
+ { text: 'Keep Editing', style: 'cancel' },
+ { text: 'Discard', style: 'destructive', onPress: () => navigation.goBack() }
+ ]
+ );
+ } else {
+ navigation.goBack();
+ }
+ };
+
+ const handleAvatarSelected = (newAvatarUrl: string) => {
+ setAvatarUrl(newAvatarUrl);
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+ Loading profile...
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Cancel
+
+
+
+ Edit Profile
+
+
+ {saving ? (
+
+ ) : (
+
+ Save
+
+ )}
+
+
+
+
+ {/* Avatar Section */}
+
+ setAvatarModalVisible(true)}
+ style={styles.avatarButton}
+ testID="edit-profile-avatar-button"
+ >
+
+ {avatarUrl ? (
+ {getEmojiFromAvatarId(avatarUrl)}
+ ) : (
+
+ {fullName?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || '?'}
+
+ )}
+
+
+ 📷
+
+
+
+ Change Avatar
+
+
+
+ {/* Form Section */}
+
+ {/* Display Name */}
+
+
+ Display Name
+
+
+
+ This is how other users will see you
+
+
+
+ {/* Email (Read-only) */}
+
+
+ Email
+
+
+
+ {user?.email || 'N/A'}
+
+ 🔒
+
+
+ Email cannot be changed
+
+
+
+
+
+
+ {/* Avatar Picker Modal */}
+ setAvatarModalVisible(false)}
+ currentAvatar={avatarUrl || undefined}
+ onAvatarSelected={handleAvatarSelected}
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ keyboardAvoid: {
+ flex: 1,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: 12,
+ },
+ loadingText: {
+ fontSize: 14,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ },
+ headerButton: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ headerTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ saveButton: {
+ color: KurdistanColors.kesk,
+ fontWeight: '600',
+ },
+ saveButtonDisabled: {
+ opacity: 0.4,
+ },
+ content: {
+ flex: 1,
+ },
+ contentContainer: {
+ padding: 24,
+ },
+ avatarSection: {
+ alignItems: 'center',
+ marginBottom: 32,
+ },
+ avatarButton: {
+ position: 'relative',
+ },
+ avatarCircle: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ justifyContent: 'center',
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 4,
+ },
+ avatarEmoji: {
+ fontSize: 70,
+ },
+ avatarInitial: {
+ fontSize: 48,
+ fontWeight: 'bold',
+ },
+ editAvatarBadge: {
+ position: 'absolute',
+ bottom: 4,
+ right: 4,
+ backgroundColor: '#FFFFFF',
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ justifyContent: 'center',
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.2,
+ shadowRadius: 4,
+ elevation: 4,
+ },
+ editAvatarIcon: {
+ fontSize: 18,
+ },
+ changePhotoText: {
+ marginTop: 12,
+ fontWeight: '500',
+ },
+ formSection: {
+ gap: 24,
+ },
+ inputGroup: {
+ gap: 8,
+ },
+ inputLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ marginLeft: 4,
+ },
+ textInput: {
+ borderWidth: 1,
+ borderRadius: 12,
+ padding: 16,
+ fontSize: 16,
+ },
+ inputHint: {
+ fontSize: 12,
+ marginLeft: 4,
+ },
+ readOnlyField: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderWidth: 1,
+ borderRadius: 12,
+ padding: 16,
+ },
+ readOnlyText: {
+ fontSize: 16,
+ flex: 1,
+ },
+ lockIcon: {
+ fontSize: 16,
+ marginLeft: 8,
+ },
+});
+
+export default EditProfileScreen;
diff --git a/mobile/src/screens/ProfileScreen.tsx b/mobile/src/screens/ProfileScreen.tsx
index 940300a9..652739b0 100644
--- a/mobile/src/screens/ProfileScreen.tsx
+++ b/mobile/src/screens/ProfileScreen.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
@@ -10,15 +10,33 @@ import {
Image,
ActivityIndicator,
Alert,
+ Platform,
} from 'react-native';
+import { useFocusEffect } from '@react-navigation/native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { useAuth } from '../contexts/AuthContext';
+import { useTheme } from '../contexts/ThemeContext';
import { KurdistanColors } from '../theme/colors';
import { supabase } from '../lib/supabase';
import AvatarPickerModal from '../components/AvatarPickerModal';
+// Cross-platform alert helper
+const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void; style?: string}>) => {
+ if (Platform.OS === 'web') {
+ if (buttons && buttons.length > 1) {
+ const result = window.confirm(`${title}\n\n${message}`);
+ if (result && buttons[1]?.onPress) {
+ buttons[1].onPress();
+ }
+ } else {
+ window.alert(`${title}\n\n${message}`);
+ }
+ } else {
+ Alert.alert(title, message, buttons as any);
+ }
+};
+
// Avatar pool matching AvatarPickerModal
const AVATAR_POOL = [
{ id: 'avatar1', emoji: '👨🏻' },
@@ -65,16 +83,19 @@ interface ProfileData {
}
const ProfileScreen: React.FC = () => {
- const { t } = useTranslation();
- const navigation = useNavigation();
+ const navigation = useNavigation();
const { user, signOut } = useAuth();
+ const { isDarkMode, colors, fontScale } = useTheme();
const [profileData, setProfileData] = useState(null);
const [loading, setLoading] = useState(true);
const [avatarModalVisible, setAvatarModalVisible] = useState(false);
- useEffect(() => {
- fetchProfileData();
- }, [user]);
+ // Refresh profile data when screen is focused (e.g., after EditProfile)
+ useFocusEffect(
+ useCallback(() => {
+ fetchProfileData();
+ }, [user])
+ );
const fetchProfileData = async () => {
if (!user) {
@@ -100,7 +121,7 @@ const ProfileScreen: React.FC = () => {
};
const handleLogout = () => {
- Alert.alert(
+ showAlert(
'Logout',
'Are you sure you want to logout?',
[
@@ -120,12 +141,22 @@ const ProfileScreen: React.FC = () => {
setProfileData(prev => prev ? { ...prev, avatar_url: avatarUrl } : null);
};
- const ProfileCard = ({ icon, title, value, onPress }: { icon: string; title: string; value: string; onPress?: () => void }) => (
-
+ const handleEditProfile = () => {
+ navigation.navigate('EditProfile');
+ };
+
+ const ProfileCard = ({ icon, title, value, onPress, testID }: { icon: string; title: string; value: string; onPress?: () => void; testID?: string }) => (
+
{icon}
- {title}
- {value}
+ {title}
+ {value}
{onPress && →}
@@ -133,41 +164,42 @@ const ProfileScreen: React.FC = () => {
if (loading) {
return (
-
+
-
+
);
}
return (
-
-
+
+
-
+
{/* Header with Gradient */}
- setAvatarModalVisible(true)} style={styles.avatarWrapper}>
+ setAvatarModalVisible(true)} style={styles.avatarWrapper} testID="profile-avatar-button">
{profileData?.avatar_url ? (
// Check if avatar_url is a URL (starts with http) or an emoji ID
profileData.avatar_url.startsWith('http') ? (
-
+
) : (
// It's an emoji ID, render as emoji text
-
-
+
+
{getEmojiFromAvatarId(profileData.avatar_url)}
)
) : (
-
-
+
+
{profileData?.full_name?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || '?'}
@@ -176,25 +208,27 @@ const ProfileScreen: React.FC = () => {
📷
-
+
{profileData?.full_name || user?.email?.split('@')[0] || 'User'}
- {user?.email}
+ {user?.email}
{/* Profile Info Cards */}
-
+
{
title="Referrals"
value={`${profileData?.referral_count || 0} people`}
onPress={() => (navigation as any).navigate('Referral')}
+ testID="profile-card-referrals"
/>
{profileData?.referral_code && (
@@ -209,6 +244,7 @@ const ProfileScreen: React.FC = () => {
icon="🎁"
title="Your Referral Code"
value={profileData.referral_code}
+ testID="profile-card-referral-code"
/>
)}
@@ -217,31 +253,34 @@ const ProfileScreen: React.FC = () => {
icon="👛"
title="Wallet Address"
value={`${profileData.wallet_address.slice(0, 10)}...${profileData.wallet_address.slice(-8)}`}
+ testID="profile-card-wallet"
/>
)}
{/* Action Buttons */}
-
+
Alert.alert('Coming Soon', 'Edit profile feature will be available soon')}
+ style={[styles.actionButton, { backgroundColor: colors.surface }]}
+ onPress={handleEditProfile}
+ testID="profile-edit-button"
>
✏️
- Edit Profile
+ Edit Profile
→
Alert.alert(
+ style={[styles.actionButton, { backgroundColor: colors.surface }]}
+ onPress={() => showAlert(
'About Pezkuwi',
'Pezkuwi is a decentralized blockchain platform for Digital Kurdistan.\n\nVersion: 1.0.0\n\n© 2026 Digital Kurdistan',
[{ text: 'OK' }]
)}
+ testID="profile-about-button"
>
ℹ️
- About Pezkuwi
+ About Pezkuwi
→
@@ -251,15 +290,16 @@ const ProfileScreen: React.FC = () => {
style={styles.logoutButton}
onPress={handleLogout}
activeOpacity={0.8}
+ testID="profile-logout-button"
>
- Logout
+ Logout
-
-
+
+
Pezkuwi Blockchain • {new Date().getFullYear()}
- Version 1.0.0
+ Version 1.0.0
diff --git a/mobile/src/screens/ReferralScreen.tsx b/mobile/src/screens/ReferralScreen.tsx
index e538f04e..f8318ba1 100644
--- a/mobile/src/screens/ReferralScreen.tsx
+++ b/mobile/src/screens/ReferralScreen.tsx
@@ -13,7 +13,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { KurdistanColors } from '../theme/colors';
import {
@@ -41,7 +40,6 @@ interface Referral {
}
const ReferralScreen: React.FC = () => {
- const { t: _t } = useTranslation();
const { selectedAccount, api, connectWallet, isApiReady } = usePezkuwi();
const isConnected = !!selectedAccount;
diff --git a/mobile/src/screens/SettingsScreen.tsx b/mobile/src/screens/SettingsScreen.tsx
index 65a3da56..dfc7b267 100644
--- a/mobile/src/screens/SettingsScreen.tsx
+++ b/mobile/src/screens/SettingsScreen.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
@@ -9,289 +9,646 @@ import {
StatusBar,
Alert,
Switch,
+ Linking,
+ ActivityIndicator,
+ Modal,
+ TextInput,
+ Platform
} from 'react-native';
-import { useTranslation } from 'react-i18next';
+import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
-import TermsOfServiceModal from '../components/TermsOfServiceModal';
-import PrivacyPolicyModal from '../components/PrivacyPolicyModal';
-import EmailNotificationsModal from '../components/EmailNotificationsModal';
-import ChangePasswordModal from '../components/ChangePasswordModal';
-import FontSizeModal from '../components/FontSizeModal';
import { useTheme } from '../contexts/ThemeContext';
import { useBiometricAuth } from '../contexts/BiometricAuthContext';
import { useAuth } from '../contexts/AuthContext';
+import { usePezkuwi, NETWORKS } from '../contexts/PezkuwiContext';
+import { supabase } from '../lib/supabase';
-const SettingsScreen: React.FC = () => {
- const { t } = useTranslation();
- const navigation = useNavigation();
- const { isDarkMode, toggleDarkMode, colors, fontSize, setFontSize } = useTheme();
- const { isBiometricAvailable, isBiometricEnabled, enableBiometric, disableBiometric, biometricType } = useBiometricAuth();
- const { changePassword } = useAuth();
-
- // Settings state
- const [notificationsEnabled, setNotificationsEnabled] = useState(true);
-
- // Modal state
- const [showTerms, setShowTerms] = useState(false);
- const [showPrivacy, setShowPrivacy] = useState(false);
- const [showEmailPrefs, setShowEmailPrefs] = useState(false);
- const [showChangePassword, setShowChangePassword] = useState(false);
- const [showFontSize, setShowFontSize] = useState(false);
-
- // Create styles with current theme colors
- const styles = React.useMemo(() => createStyles(colors), [colors]);
-
- React.useEffect(() => {
- console.log('[Settings] Screen mounted');
- console.log('[Settings] isDarkMode:', isDarkMode);
- console.log('[Settings] fontSize:', fontSize);
- console.log('[Settings] isBiometricEnabled:', isBiometricEnabled);
- console.log('[Settings] styles:', styles ? 'DEFINED' : 'UNDEFINED');
- }, []);
-
- const handleBiometryToggle = async (value: boolean) => {
- if (value) {
- // Check if biometric is available
- if (!isBiometricAvailable) {
- Alert.alert(
- t('biometricAuth'),
- 'Biometric authentication is not available on this device. Please enroll fingerprint or face ID in your device settings.'
- );
- return;
- }
-
- // Try to enable biometric directly
- const success = await enableBiometric();
- if (success) {
- Alert.alert(t('settingsScreen.biometricAlerts.successTitle'), t('settingsScreen.biometricAlerts.enabled'));
- } else {
- Alert.alert('Error', 'Failed to enable biometric authentication. Please try again.');
+// Cross-platform alert helper
+const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void; style?: string}>) => {
+ if (Platform.OS === 'web') {
+ if (buttons && buttons.length > 1) {
+ // For confirm dialogs
+ const result = window.confirm(`${title}\n\n${message}`);
+ if (result && buttons[1]?.onPress) {
+ buttons[1].onPress();
}
} else {
- await disableBiometric();
- Alert.alert(t('settingsScreen.biometricAlerts.successTitle'), t('settingsScreen.biometricAlerts.disabled'));
+ window.alert(`${title}\n\n${message}`);
+ }
+ } else {
+ Alert.alert(title, message, buttons as any);
+ }
+};
+
+// Font size options
+type FontSize = 'small' | 'medium' | 'large';
+const FONT_SIZE_OPTIONS: { value: FontSize; label: string; description: string }[] = [
+ { value: 'small', label: 'Small', description: '87.5% - Compact text' },
+ { value: 'medium', label: 'Medium', description: '100% - Default size' },
+ { value: 'large', label: 'Large', description: '112.5% - Easier to read' },
+];
+
+// Auto-lock timer options (in minutes)
+const AUTO_LOCK_OPTIONS: { value: number; label: string }[] = [
+ { value: 1, label: '1 minute' },
+ { value: 5, label: '5 minutes' },
+ { value: 15, label: '15 minutes' },
+ { value: 30, label: '30 minutes' },
+ { value: 60, label: '1 hour' },
+];
+
+// --- COMPONENTS (Internal for simplicity) ---
+
+const SectionHeader = ({ title }: { title: string }) => {
+ const { colors } = useTheme();
+ return (
+
+ {title}
+
+ );
+};
+
+const SettingItem = ({
+ icon,
+ title,
+ subtitle,
+ onPress,
+ showArrow = true,
+ textColor,
+ testID
+}: {
+ icon: string;
+ title: string;
+ subtitle?: string;
+ onPress: () => void;
+ showArrow?: boolean;
+ textColor?: string;
+ testID?: string;
+}) => {
+ const { colors } = useTheme();
+ return (
+
+
+ {icon}
+
+
+ {title}
+ {subtitle && {subtitle}}
+
+ {showArrow && →}
+
+ );
+};
+
+const SettingToggle = ({
+ icon,
+ title,
+ subtitle,
+ value,
+ onToggle,
+ loading = false,
+ testID
+}: {
+ icon: string;
+ title: string;
+ subtitle?: string;
+ value: boolean;
+ onToggle: (value: boolean) => void;
+ loading?: boolean;
+ testID?: string;
+}) => {
+ const { colors } = useTheme();
+ return (
+
+
+ {icon}
+
+
+ {title}
+ {subtitle && {subtitle}}
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+// --- MAIN SCREEN ---
+
+const SettingsScreen: React.FC = () => {
+ const navigation = useNavigation();
+ const { isDarkMode, toggleDarkMode, colors, fontSize, setFontSize } = useTheme();
+ const { isBiometricEnabled, enableBiometric, disableBiometric, biometricType, autoLockTimer, setAutoLockTimer } = useBiometricAuth();
+ const { signOut, user } = useAuth();
+ const { currentNetwork, switchNetwork } = usePezkuwi();
+
+ // Profile State (Supabase)
+ const [profile, setProfile] = useState({
+ full_name: '',
+ username: '',
+ notifications_push: false,
+ notifications_email: true,
+ });
+ const [loadingProfile, setLoadingProfile] = useState(false);
+ const [savingSettings, setSavingSettings] = useState(false);
+
+ // Modals
+ const [showNetworkModal, setShowNetworkModal] = useState(false);
+ const [showProfileEdit, setShowProfileEdit] = useState(false);
+ const [showFontSizeModal, setShowFontSizeModal] = useState(false);
+ const [showAutoLockModal, setShowAutoLockModal] = useState(false);
+ const [editName, setEditName] = useState('');
+ const [editBio, setEditBio] = useState('');
+
+ // 1. Fetch Profile from Supabase
+ const fetchProfile = useCallback(async () => {
+ if (!user) return;
+ setLoadingProfile(true);
+ try {
+ const { data, error } = await supabase
+ .from('profiles')
+ .select('*')
+ .eq('id', user.id)
+ .maybeSingle();
+
+ if (error) throw error;
+ if (data) {
+ setProfile(data);
+ setEditName(data.full_name || '');
+ setEditBio(data.bio || '');
+ }
+ } catch (err) {
+ console.log('Error fetching profile:', err);
+ } finally {
+ setLoadingProfile(false);
+ }
+ }, [user]);
+
+ useEffect(() => {
+ fetchProfile();
+ }, [fetchProfile]);
+
+ // 2. Update Settings in Supabase
+ const updateSetting = async (key: string, value: boolean) => {
+ if (!user) return;
+ setSavingSettings(true);
+
+ // Optimistic update
+ setProfile((prev: any) => ({ ...prev, [key]: value }));
+
+ try {
+ const { error } = await supabase
+ .from('profiles')
+ .update({ [key]: value, updated_at: new Date().toISOString() })
+ .eq('id', user.id);
+
+ if (error) throw error;
+ } catch (err) {
+ console.error('Failed to update setting:', err);
+ // Revert on error
+ setProfile((prev: any) => ({ ...prev, [key]: !value }));
+ showAlert('Error', 'Failed to save setting. Please check your connection.');
+ } finally {
+ setSavingSettings(false);
}
};
- const SettingItem = ({
- icon,
- title,
- subtitle,
- onPress,
- showArrow = true,
- }: {
- icon: string;
- title: string;
- subtitle?: string;
- onPress: () => void;
- showArrow?: boolean;
- }) => (
- {
- console.log(`[Settings] Button pressed: ${title}`);
- onPress();
- }}
- >
-
- {icon}
-
-
- {title}
- {subtitle && {subtitle}}
-
- {showArrow && →}
-
- );
+ // 3. Save Profile Info
+ const saveProfileInfo = async () => {
+ if (!user) return;
+ try {
+ const { error } = await supabase
+ .from('profiles')
+ .update({
+ full_name: editName,
+ bio: editBio,
+ updated_at: new Date().toISOString()
+ })
+ .eq('id', user.id);
- const SettingToggle = ({
- icon,
- title,
- subtitle,
- value,
- onToggle,
- testID,
- }: {
- icon: string;
- title: string;
- subtitle?: string;
- value: boolean;
- onToggle: (value: boolean) => void;
- testID?: string;
- }) => (
-
-
- {icon}
-
-
- {title}
- {subtitle && {subtitle}}
-
-
-
- );
+ if (error) throw error;
+
+ setProfile((prev: any) => ({ ...prev, full_name: editName, bio: editBio }));
+ setShowProfileEdit(false);
+ showAlert('Success', 'Profile updated successfully');
+ } catch (err) {
+ showAlert('Error', 'Failed to update profile');
+ }
+ };
+
+ // 4. Biometric Handler
+ const handleBiometryToggle = async (value: boolean) => {
+ // Biometric not available on web
+ if (Platform.OS === 'web') {
+ showAlert(
+ 'Not Available',
+ 'Biometric authentication is only available on mobile devices.'
+ );
+ return;
+ }
+
+ if (value) {
+ const success = await enableBiometric();
+ if (success) {
+ showAlert('Success', 'Biometric authentication enabled');
+ }
+ } else {
+ await disableBiometric();
+ }
+ };
+
+ // 5. Network Switcher
+ const handleNetworkChange = async (network: 'pezkuwi' | 'bizinikiwi') => {
+ await switchNetwork(network);
+ setShowNetworkModal(false);
+
+ showAlert(
+ 'Network Changed',
+ `Switched to ${NETWORKS[network].displayName}. The app will reconnect automatically.`,
+ [{ text: 'OK' }]
+ );
+ };
+
+ // 6. Font Size Handler
+ const handleFontSizeChange = async (size: FontSize) => {
+ await setFontSize(size);
+ setShowFontSizeModal(false);
+ };
+
+ // 7. Auto-Lock Timer Handler
+ const handleAutoLockChange = async (minutes: number) => {
+ await setAutoLockTimer(minutes);
+ setShowAutoLockModal(false);
+ };
+
+ // Get display text for current font size
+ const getFontSizeLabel = () => {
+ const option = FONT_SIZE_OPTIONS.find(opt => opt.value === fontSize);
+ return option ? option.label : 'Medium';
+ };
+
+ // Get display text for current auto-lock timer
+ const getAutoLockLabel = () => {
+ const option = AUTO_LOCK_OPTIONS.find(opt => opt.value === autoLockTimer);
+ return option ? option.label : '5 minutes';
+ };
return (
-
+
{/* Header */}
-
+
navigation.goBack()} style={styles.backButton}>
←
- {t('settings')}
-
+ Settings
+
- {/* Appearance Section */}
-
- APPEARANCE
+
+ {/* ACCOUNT SECTION */}
+
+
+ setShowProfileEdit(true)}
+ testID="edit-profile-button"
+ />
+ navigation.navigate('Wallet')}
+ testID="wallet-management-button"
+ />
+
+ {/* APP SETTINGS */}
+
+
{
- await toggleDarkMode();
- }}
- testID="dark-mode-switch"
+ onToggle={toggleDarkMode}
+ testID="dark-mode"
/>
setShowFontSize(true)}
+ subtitle={getFontSizeLabel()}
+ onPress={() => setShowFontSizeModal(true)}
+ testID="font-size-button"
/>
-
-
- {/* Security Section */}
-
- {t('security').toUpperCase()}
-
-
-
- setShowChangePassword(true)}
- />
-
-
- {/* Notifications Section */}
-
- {t('notifications').toUpperCase()}
updateSetting('notifications_push', val)}
+ loading={savingSettings}
+ testID="push-notifications"
/>
- setShowEmailPrefs(true)}
+ title="Email Updates"
+ subtitle="Receive newsletters & reports"
+ value={profile.notifications_email}
+ onToggle={(val) => updateSetting('notifications_email', val)}
+ loading={savingSettings}
+ testID="email-updates"
/>
- {/* About Section */}
-
- {t('about').toUpperCase()}
-
+ {/* NETWORK & SECURITY */}
+
+
Alert.alert(
- t('about'),
- t('appName') + '\n\n' + t('version') + ': 1.0.0',
- [{ text: t('common.confirm') }]
- )}
+ icon="📡"
+ title="Network Node"
+ subtitle={NETWORKS[currentNetwork]?.displayName || 'Unknown'}
+ onPress={() => setShowNetworkModal(true)}
+ testID="network-node-button"
/>
+
+
+ setShowAutoLockModal(true)}
+ testID="auto-lock-button"
+ />
+
+
+ {/* SUPPORT */}
+
+
setShowTerms(true)}
+ title="Terms of Service"
+ onPress={() => showAlert('Terms', 'Terms of service content...')}
+ testID="terms-of-service-button"
/>
-
setShowPrivacy(true)}
+ title="Privacy Policy"
+ onPress={() => showAlert('Privacy', 'Privacy policy content...')}
+ testID="privacy-policy-button"
/>
-
Alert.alert(t('help'), 'support@pezkuwichain.io')}
+ icon="❓"
+ title="Help Center"
+ onPress={() => Linking.openURL('mailto:support@pezkuwichain.io')}
+ testID="help-center-button"
/>
-
- {t('appName')}
- {t('version')} 1.0.0
- © 2026 Digital Kurdistan
+ {/* DEVELOPER OPTIONS (only in DEV) */}
+ {__DEV__ && (
+
+ DEVELOPER
+ {
+ showAlert(
+ 'Reset Wallet',
+ 'This will delete all wallet data including saved accounts and keys. Are you sure?',
+ [
+ { text: 'Cancel', style: 'cancel' },
+ {
+ text: 'Reset',
+ style: 'destructive',
+ onPress: async () => {
+ try {
+ await AsyncStorage.multiRemove([
+ '@pezkuwi_wallets',
+ '@pezkuwi_selected_account',
+ '@pezkuwi_selected_network'
+ ]);
+ showAlert('Success', 'Wallet data cleared. Restart the app to see changes.');
+ } catch (error) {
+ showAlert('Error', 'Failed to clear wallet data');
+ }
+ }
+ }
+ ]
+ );
+ }}
+ testID="reset-wallet-button"
+ />
+
+ )}
+
+ {/* LOGOUT */}
+
+ {
+ showAlert(
+ 'Sign Out',
+ 'Are you sure you want to sign out?',
+ [
+ { text: 'Cancel', style: 'cancel' },
+ { text: 'Sign Out', style: 'destructive', onPress: signOut }
+ ]
+ );
+ }}
+ testID="sign-out-button"
+ />
+
+ Pezkuwi Super App v1.0.0
+ © 2026 Digital Kurdistan
+
- {/* Modals */}
- setShowTerms(false)}
- />
+ {/* NETWORK MODAL */}
+
+
+
+ Select Network
- setShowPrivacy(false)}
- />
+ handleNetworkChange('pezkuwi')}
+ testID="network-option-mainnet"
+ >
+ {NETWORKS.pezkuwi.displayName}
+ {NETWORKS.pezkuwi.rpcEndpoint}
+
- setShowEmailPrefs(false)}
- />
+ handleNetworkChange('bizinikiwi')}
+ testID="network-option-testnet"
+ >
+ {NETWORKS.bizinikiwi.displayName}
+ {NETWORKS.bizinikiwi.rpcEndpoint}
+
- setShowChangePassword(false)}
- />
+ handleNetworkChange('zombienet')}
+ testID="network-option-zombienet"
+ >
+ {NETWORKS.zombienet.displayName}
+ {NETWORKS.zombienet.rpcEndpoint}
+
+
+ setShowNetworkModal(false)}
+ testID="network-modal-cancel"
+ >
+ Cancel
+
+
+
+
+
+ {/* PROFILE EDIT MODAL */}
+
+
+
+ setShowProfileEdit(false)}>
+ Cancel
+
+ Edit Profile
+
+ Save
+
+
+
+
+ Full Name
+
+
+ Bio
+
+
+
+
+
+ {/* FONT SIZE MODAL */}
+
+
+
+ Select Font Size
+
+ {FONT_SIZE_OPTIONS.map((option) => (
+ handleFontSizeChange(option.value)}
+ testID={`font-size-option-${option.value}`}
+ >
+ {option.label}
+ {option.description}
+
+ ))}
+
+ setShowFontSizeModal(false)}
+ testID="font-size-modal-cancel"
+ >
+ Cancel
+
+
+
+
+
+ {/* AUTO-LOCK TIMER MODAL */}
+
+
+
+ Auto-Lock Timer
+
+ Lock app after inactivity
+
+
+ {AUTO_LOCK_OPTIONS.map((option) => (
+ handleAutoLockChange(option.value)}
+ testID={`auto-lock-option-${option.value}`}
+ >
+ {option.label}
+
+ ))}
+
+ setShowAutoLockModal(false)}
+ testID="auto-lock-modal-cancel"
+ >
+ Cancel
+
+
+
+
- setShowFontSize(false)}
- />
);
};
-const createStyles = (colors: any) => StyleSheet.create({
+const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: colors.background,
},
header: {
flexDirection: 'row',
@@ -299,9 +656,7 @@ const createStyles = (colors: any) => StyleSheet.create({
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
- backgroundColor: colors.surface,
borderBottomWidth: 1,
- borderBottomColor: colors.border,
},
backButton: {
width: 40,
@@ -316,82 +671,134 @@ const createStyles = (colors: any) => StyleSheet.create({
headerTitle: {
fontSize: 18,
fontWeight: 'bold',
- color: colors.text,
},
- placeholder: {
- width: 40,
- },
- section: {
+ sectionHeader: {
marginTop: 24,
- backgroundColor: colors.surface,
- borderRadius: 12,
- marginHorizontal: 16,
- padding: 16,
- boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)',
- elevation: 2,
+ marginBottom: 8,
+ paddingHorizontal: 16,
},
sectionTitle: {
fontSize: 12,
fontWeight: '700',
- color: colors.textSecondary,
- marginBottom: 12,
letterSpacing: 0.5,
},
+ section: {
+ marginHorizontal: 16,
+ borderRadius: 12,
+ overflow: 'hidden',
+ },
settingItem: {
flexDirection: 'row',
alignItems: 'center',
- paddingVertical: 12,
+ padding: 16,
borderBottomWidth: 1,
- borderBottomColor: colors.border,
},
settingIcon: {
- width: 40,
- height: 40,
- borderRadius: 20,
- backgroundColor: colors.background,
+ width: 32,
+ height: 32,
+ borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
settingIconText: {
- fontSize: 20,
+ fontSize: 18,
},
settingContent: {
flex: 1,
},
settingTitle: {
fontSize: 16,
- fontWeight: '600',
- color: colors.text,
- marginBottom: 2,
+ fontWeight: '500',
},
settingSubtitle: {
fontSize: 13,
- color: colors.textSecondary,
+ marginTop: 2,
},
arrow: {
fontSize: 18,
- color: colors.textSecondary,
},
- versionContainer: {
+ footer: {
alignItems: 'center',
- paddingVertical: 24,
+ marginTop: 32,
},
versionText: {
- fontSize: 14,
+ fontSize: 13,
fontWeight: '600',
- color: colors.textSecondary,
- },
- versionNumber: {
- fontSize: 12,
- color: colors.textSecondary,
- marginTop: 4,
},
copyright: {
fontSize: 11,
- color: colors.textSecondary,
marginTop: 4,
},
+ // Modal Styles
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ justifyContent: 'center',
+ padding: 20,
+ },
+ modalContent: {
+ borderRadius: 16,
+ padding: 20,
+ },
+ modalTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 20,
+ textAlign: 'center',
+ },
+ modalSubtitle: {
+ fontSize: 13,
+ textAlign: 'center',
+ marginTop: -12,
+ marginBottom: 16,
+ },
+ networkOption: {
+ padding: 16,
+ borderRadius: 12,
+ backgroundColor: '#f5f5f5',
+ marginBottom: 10,
+ borderWidth: 1,
+ borderColor: '#eee',
+ },
+ selectedNetwork: {
+ borderColor: KurdistanColors.kesk,
+ backgroundColor: '#e8f5e9',
+ },
+ networkName: {
+ fontWeight: 'bold',
+ fontSize: 16,
+ color: '#333',
+ },
+ networkUrl: {
+ fontSize: 12,
+ color: '#666',
+ marginTop: 2,
+ },
+ cancelButton: {
+ marginTop: 10,
+ padding: 16,
+ alignItems: 'center',
+ },
+ cancelText: {
+ color: '#FF3B30',
+ fontWeight: '600',
+ fontSize: 16,
+ },
+ fullModal: {
+ flex: 1,
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: '600',
+ marginBottom: 8,
+ },
+ input: {
+ borderWidth: 1,
+ borderRadius: 8,
+ padding: 12,
+ fontSize: 16,
+ },
});
-export default SettingsScreen;
+export default SettingsScreen;
\ No newline at end of file
diff --git a/mobile/src/screens/SignInScreen.tsx b/mobile/src/screens/SignInScreen.tsx
index 6b1a7428..d25486e0 100644
--- a/mobile/src/screens/SignInScreen.tsx
+++ b/mobile/src/screens/SignInScreen.tsx
@@ -14,7 +14,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { useAuth } from '../contexts/AuthContext';
import { KurdistanColors } from '../theme/colors';
@@ -24,7 +23,6 @@ interface SignInScreenProps {
}
const SignInScreen: React.FC = ({ onSignIn, onNavigateToSignUp }) => {
- const { t } = useTranslation();
const { signIn } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -78,17 +76,17 @@ const SignInScreen: React.FC = ({ onSignIn, onNavigateToSignU
PZK
- {t('auth.welcomeBack')}
- {t('auth.signIn')}
+ Welcome Back!
+ Sign In
{/* Form */}
- {t('auth.email')}
+ Email
= ({ onSignIn, onNavigateToSignU
- {t('auth.password')}
+ Password
= ({ onSignIn, onNavigateToSignU
- {t('auth.forgotPassword')}
+ Forgot Password?
@@ -124,7 +122,7 @@ const SignInScreen: React.FC = ({ onSignIn, onNavigateToSignU
{isLoading ? (
) : (
- {t('auth.signIn')}
+ Sign In
)}
@@ -139,8 +137,8 @@ const SignInScreen: React.FC = ({ onSignIn, onNavigateToSignU
onPress={onNavigateToSignUp}
>
- {t('auth.noAccount')}{' '}
- {t('auth.signUp')}
+ Don't have an account?{' '}
+ Sign Up
diff --git a/mobile/src/screens/SignUpScreen.tsx b/mobile/src/screens/SignUpScreen.tsx
index baafff72..1f9b052b 100644
--- a/mobile/src/screens/SignUpScreen.tsx
+++ b/mobile/src/screens/SignUpScreen.tsx
@@ -14,7 +14,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { useAuth } from '../contexts/AuthContext';
import { KurdistanColors } from '../theme/colors';
@@ -24,7 +23,6 @@ interface SignUpScreenProps {
}
const SignUpScreen: React.FC = ({ onSignUp, onNavigateToSignIn }) => {
- const { t } = useTranslation();
const { signUp } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -85,17 +83,17 @@ const SignUpScreen: React.FC = ({ onSignUp, onNavigateToSignI
PZK
- {t('auth.getStarted')}
- {t('auth.createAccount')}
+ Get Started
+ Create Account
{/* Form */}
- {t('auth.email')}
+ Email
= ({ onSignUp, onNavigateToSignI
- {t('auth.username')}
+ Username
= ({ onSignUp, onNavigateToSignI
- {t('auth.password')}
+ Password
= ({ onSignUp, onNavigateToSignI
- {t('auth.confirmPassword')}
+ Confirm Password
= ({ onSignUp, onNavigateToSignI
{isLoading ? (
) : (
- {t('auth.signUp')}
+ Sign Up
)}
@@ -164,8 +162,8 @@ const SignUpScreen: React.FC = ({ onSignUp, onNavigateToSignI
onPress={onNavigateToSignIn}
>
- {t('auth.haveAccount')}{' '}
- {t('auth.signIn')}
+ Already have an account?{' '}
+ Sign In
diff --git a/mobile/src/screens/StakingScreen.tsx b/mobile/src/screens/StakingScreen.tsx
index 885f88c7..c25a2f70 100644
--- a/mobile/src/screens/StakingScreen.tsx
+++ b/mobile/src/screens/StakingScreen.tsx
@@ -42,7 +42,7 @@ const SCORE_WEIGHTS = {
};
export default function StakingScreen() {
- const { api, selectedAccount, isApiReady } = usePezkuwi();
+ const { api, selectedAccount, isApiReady, getKeyPair } = usePezkuwi();
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
@@ -137,17 +137,20 @@ export default function StakingScreen() {
setProcessing(true);
if (!api || !selectedAccount) return;
+ // Get keypair for signing
+ const keyPair = await getKeyPair(selectedAccount.address);
+ if (!keyPair) {
+ Alert.alert('Error', 'Could not retrieve wallet keypair for signing');
+ return;
+ }
+
// Convert amount to planck
const amountPlanck = BigInt(Math.floor(parseFloat(stakeAmount) * 1e12));
// Bond tokens (or bond_extra if already bonding)
- // For simplicity, using bond_extra if already bonded, otherwise bond
- // But UI should handle controller/stash logic. Assuming simple setup.
- // This part is simplified.
-
const tx = api.tx.staking.bondExtra(amountPlanck);
-
- await tx.signAndSend(selectedAccount.address, ({ status }) => {
+
+ await tx.signAndSend(keyPair, ({ status }) => {
if (status.isInBlock) {
Alert.alert('Success', `Successfully staked ${stakeAmount} HEZ!`);
setStakeSheetVisible(false);
@@ -173,10 +176,17 @@ export default function StakingScreen() {
setProcessing(true);
if (!api || !selectedAccount) return;
+ // Get keypair for signing
+ const keyPair = await getKeyPair(selectedAccount.address);
+ if (!keyPair) {
+ Alert.alert('Error', 'Could not retrieve wallet keypair for signing');
+ return;
+ }
+
const amountPlanck = BigInt(Math.floor(parseFloat(unstakeAmount) * 1e12));
const tx = api.tx.staking.unbond(amountPlanck);
- await tx.signAndSend(selectedAccount.address, ({ status }) => {
+ await tx.signAndSend(keyPair, ({ status }) => {
if (status.isInBlock) {
Alert.alert(
'Success',
@@ -200,10 +210,17 @@ export default function StakingScreen() {
setProcessing(true);
if (!api || !selectedAccount) return;
+ // Get keypair for signing
+ const keyPair = await getKeyPair(selectedAccount.address);
+ if (!keyPair) {
+ Alert.alert('Error', 'Could not retrieve wallet keypair for signing');
+ return;
+ }
+
// Withdraw all available unbonded funds
// num_slashing_spans is usually 0 for simple stakers
const tx = api.tx.staking.withdrawUnbonded(0);
- await tx.signAndSend(selectedAccount.address, ({ status }) => {
+ await tx.signAndSend(keyPair, ({ status }) => {
if (status.isInBlock) {
Alert.alert('Success', 'Successfully withdrawn unbonded tokens!');
fetchStakingData();
@@ -226,8 +243,16 @@ export default function StakingScreen() {
setProcessing(true);
try {
+ // Get keypair for signing
+ const keyPair = await getKeyPair(selectedAccount.address);
+ if (!keyPair) {
+ Alert.alert('Error', 'Could not retrieve wallet keypair for signing');
+ setProcessing(false);
+ return;
+ }
+
const tx = api.tx.staking.nominate(validators);
- await tx.signAndSend(selectedAccount.address, ({ status }) => {
+ await tx.signAndSend(keyPair, ({ status }) => {
if (status.isInBlock) {
Alert.alert('Success', 'Nomination transaction sent!');
setValidatorSheetVisible(false);
diff --git a/mobile/src/screens/SwapScreen.tsx b/mobile/src/screens/SwapScreen.tsx
index 2ce64d26..e3c66b09 100644
--- a/mobile/src/screens/SwapScreen.tsx
+++ b/mobile/src/screens/SwapScreen.tsx
@@ -13,8 +13,10 @@ import {
Platform,
Image,
} from 'react-native';
+import { useNavigation } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi } from '../contexts/PezkuwiContext';
+import { KurdistanSun } from '../components/KurdistanSun';
// Token Images
const hezLogo = require('../../../shared/images/hez_logo.png');
@@ -30,14 +32,15 @@ interface TokenInfo {
}
const TOKENS: TokenInfo[] = [
- { symbol: 'HEZ', name: 'Hemuwelet', assetId: 0, decimals: 12, logo: hezLogo },
- { symbol: 'PEZ', name: 'Pezkunel', assetId: 1, decimals: 12, logo: pezLogo },
+ { symbol: 'HEZ', name: 'Welati Coin', assetId: 0, decimals: 12, logo: hezLogo },
+ { symbol: 'PEZ', name: 'Pezkuwichain Token', assetId: 1, decimals: 12, logo: pezLogo },
{ symbol: 'USDT', name: 'Tether USD', assetId: 1000, decimals: 6, logo: usdtLogo },
];
type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error';
const SwapScreen: React.FC = () => {
+ const navigation = useNavigation();
const { api, isApiReady, selectedAccount, getKeyPair } = usePezkuwi();
const [fromToken, setFromToken] = useState(TOKENS[0]);
@@ -49,6 +52,17 @@ const SwapScreen: React.FC = () => {
const [fromBalance, setFromBalance] = useState('0');
const [toBalance, setToBalance] = useState('0');
+ // Pool reserves for AMM calculation
+ const [poolReserves, setPoolReserves] = useState<{
+ reserve0: number;
+ reserve1: number;
+ asset0: number;
+ asset1: number;
+ } | null>(null);
+ const [exchangeRate, setExchangeRate] = useState(0);
+ const [isLoadingRate, setIsLoadingRate] = useState(false);
+ const [isDexAvailable, setIsDexAvailable] = useState(false);
+
const [txStatus, setTxStatus] = useState('idle');
const [errorMessage, setErrorMessage] = useState('');
@@ -93,18 +107,184 @@ const SwapScreen: React.FC = () => {
fetchBalances();
}, [api, isApiReady, selectedAccount, fromToken, toToken]);
- // Calculate output amount (simple 1:1 for now - should use pool reserves)
+ // Check if AssetConversion pallet is available
+ useEffect(() => {
+ if (api && isApiReady) {
+ const hasAssetConversion = api.tx.assetConversion !== undefined;
+ setIsDexAvailable(hasAssetConversion);
+ if (__DEV__ && !hasAssetConversion) {
+ console.warn('AssetConversion pallet not available in runtime');
+ }
+ }
+ }, [api, isApiReady]);
+
+ // Fetch exchange rate from AssetConversion pool
+ useEffect(() => {
+ const fetchExchangeRate = async () => {
+ if (!api || !isApiReady || !isDexAvailable) {
+ return;
+ }
+
+ setIsLoadingRate(true);
+ try {
+ // Map user-selected tokens to actual pool assets
+ // HEZ → wHEZ (Asset 0) behind the scenes
+ const getPoolAssetId = (token: TokenInfo) => {
+ if (token.symbol === 'HEZ') return 0; // wHEZ
+ return token.assetId;
+ };
+
+ const fromAssetId = getPoolAssetId(fromToken);
+ const toAssetId = getPoolAssetId(toToken);
+
+ // Pool ID must be sorted (smaller asset ID first)
+ const [asset1, asset2] = fromAssetId < toAssetId
+ ? [fromAssetId, toAssetId]
+ : [toAssetId, fromAssetId];
+
+ // Create pool asset tuple [asset1, asset2] - must be sorted!
+ const poolAssets = [
+ { NativeOrAsset: { Asset: asset1 } },
+ { NativeOrAsset: { Asset: asset2 } }
+ ];
+
+ // Query pool from AssetConversion pallet
+ const poolInfo = await api.query.assetConversion.pools(poolAssets);
+
+ if (poolInfo && !poolInfo.isEmpty) {
+ try {
+ // Derive pool account using AccountIdConverter
+ // blake2_256(&Encode::encode(&(PalletId, PoolId))[..])
+ const { stringToU8a } = await import('@pezkuwi/util');
+ const { blake2AsU8a } = await import('@pezkuwi/util-crypto');
+
+ // PalletId for AssetConversion: "py/ascon" (8 bytes)
+ const PALLET_ID = stringToU8a('py/ascon');
+
+ // Create PoolId tuple (u32, u32)
+ const poolId = api.createType('(u32, u32)', [asset1, asset2]);
+
+ // Create (PalletId, PoolId) tuple: ([u8; 8], (u32, u32))
+ const palletIdType = api.createType('[u8; 8]', PALLET_ID);
+ const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolId]);
+
+ // Hash the SCALE-encoded tuple
+ const accountHash = blake2AsU8a(fullTuple.toU8a(), 256);
+ const poolAccountId = api.createType('AccountId32', accountHash);
+
+ // Query pool account's asset balances
+ const reserve0Query = await api.query.assets.account(asset1, poolAccountId);
+ const reserve1Query = await api.query.assets.account(asset2, poolAccountId);
+
+ const reserve0Data = reserve0Query.toJSON() as { balance?: string } | null;
+ const reserve1Data = reserve1Query.toJSON() as { balance?: string } | null;
+
+ if (reserve0Data?.balance && reserve1Data?.balance) {
+ // Parse hex string balances to BigInt, then to number
+ const balance0Hex = reserve0Data.balance.toString();
+ const balance1Hex = reserve1Data.balance.toString();
+
+ // Use correct decimals for each asset
+ const decimals0 = asset1 === 1000 ? 6 : 12;
+ const decimals1 = asset2 === 1000 ? 6 : 12;
+
+ const reserve0 = Number(BigInt(balance0Hex)) / (10 ** decimals0);
+ const reserve1 = Number(BigInt(balance1Hex)) / (10 ** decimals1);
+
+ if (__DEV__) {
+ console.log('Pool reserves found:', { reserve0, reserve1, asset1, asset2 });
+ }
+
+ // Store pool reserves for AMM calculation
+ setPoolReserves({
+ reserve0,
+ reserve1,
+ asset0: asset1,
+ asset1: asset2
+ });
+
+ // Calculate simple exchange rate for display
+ const rate = fromAssetId === asset1
+ ? reserve1 / reserve0 // from asset1 to asset2
+ : reserve0 / reserve1; // from asset2 to asset1
+
+ setExchangeRate(rate);
+ } else {
+ if (__DEV__) console.warn('Pool has no reserves');
+ setExchangeRate(0);
+ setPoolReserves(null);
+ }
+ } catch (err) {
+ if (__DEV__) console.error('Error deriving pool account:', err);
+ setExchangeRate(0);
+ setPoolReserves(null);
+ }
+ } else {
+ if (__DEV__) console.warn('No liquidity pool found for this pair');
+ setExchangeRate(0);
+ setPoolReserves(null);
+ }
+ } catch (error) {
+ if (__DEV__) console.error('Failed to fetch exchange rate:', error);
+ setExchangeRate(0);
+ setPoolReserves(null);
+ } finally {
+ setIsLoadingRate(false);
+ }
+ };
+
+ fetchExchangeRate();
+ }, [api, isApiReady, isDexAvailable, fromToken, toToken]);
+
+ // Calculate output amount using Uniswap V2 AMM formula
useEffect(() => {
if (!fromAmount || parseFloat(fromAmount) <= 0) {
setToAmount('');
return;
}
- // TODO: Implement proper AMM calculation using pool reserves
- // For now, simple 1:1 conversion (placeholder)
- const calculatedAmount = (parseFloat(fromAmount) * 0.97).toFixed(6); // 3% fee simulation
- setToAmount(calculatedAmount);
- }, [fromAmount, fromToken, toToken]);
+ // If no pool reserves available, cannot calculate
+ if (!poolReserves) {
+ setToAmount('');
+ return;
+ }
+
+ const amountIn = parseFloat(fromAmount);
+ const { reserve0, reserve1, asset0 } = poolReserves;
+
+ // Determine which reserve is input and which is output
+ const getPoolAssetId = (token: TokenInfo) => {
+ if (token.symbol === 'HEZ') return 0; // wHEZ
+ return token.assetId;
+ };
+ const fromAssetId = getPoolAssetId(fromToken);
+ const isAsset0ToAsset1 = fromAssetId === asset0;
+
+ const reserveIn = isAsset0ToAsset1 ? reserve0 : reserve1;
+ const reserveOut = isAsset0ToAsset1 ? reserve1 : reserve0;
+
+ // Uniswap V2 AMM formula (matches Substrate runtime exactly)
+ // Runtime: amount_in_with_fee = amount_in * (1000 - LPFee) = amount_in * 970
+ // LPFee = 30 (3% fee)
+ // Formula: amountOut = (amountIn * 970 * reserveOut) / (reserveIn * 1000 + amountIn * 970)
+ const LP_FEE = 30; // 3% fee
+ const amountInWithFee = amountIn * (1000 - LP_FEE);
+ const numerator = amountInWithFee * reserveOut;
+ const denominator = reserveIn * 1000 + amountInWithFee;
+ const amountOut = numerator / denominator;
+
+ if (__DEV__) {
+ console.log('AMM calculation:', {
+ amountIn,
+ reserveIn,
+ reserveOut,
+ amountOut,
+ lpFee: `${LP_FEE / 10}%`
+ });
+ }
+
+ setToAmount(amountOut.toFixed(6));
+ }, [fromAmount, fromToken, toToken, poolReserves]);
// Calculate formatted balances
const fromBalanceFormatted = useMemo(() => {
@@ -161,6 +341,11 @@ const SwapScreen: React.FC = () => {
return;
}
+ if (!exchangeRate || exchangeRate === 0) {
+ Alert.alert('Error', 'No liquidity pool available for this pair');
+ return;
+ }
+
setTxStatus('signing');
setShowConfirm(false);
setErrorMessage('');
@@ -276,21 +461,22 @@ const SwapScreen: React.FC = () => {
return (
- {/* Transaction Loading Overlay */}
+ {/* Kurdistan Sun Loading Overlay */}
{(txStatus === 'signing' || txStatus === 'submitting') && (
-
-
-
- {txStatus === 'signing' ? 'Waiting for signature...' : 'Processing swap...'}
-
-
+
+
+ {txStatus === 'signing' ? 'Waiting for signature...' : 'Processing your swap...'}
+
)}
{/* Header */}
+ navigation.goBack()} style={styles.backButton}>
+ ←
+
Swap Tokens
setShowSettings(true)} style={styles.settingsButton}>
⚙️
@@ -369,7 +555,13 @@ const SwapScreen: React.FC = () => {
ℹ️ Exchange Rate
- 1 {fromToken.symbol} ≈ 1 {toToken.symbol}
+
+ {isLoadingRate
+ ? 'Loading...'
+ : exchangeRate > 0
+ ? `1 ${fromToken.symbol} ≈ ${exchangeRate.toFixed(4)} ${toToken.symbol}`
+ : 'No pool available'}
+
Slippage Tolerance
@@ -389,14 +581,16 @@ const SwapScreen: React.FC = () => {
setShowConfirm(true)}
- disabled={!fromAmount || hasInsufficientBalance || txStatus !== 'idle'}
+ disabled={!fromAmount || hasInsufficientBalance || txStatus !== 'idle' || exchangeRate === 0}
>
{hasInsufficientBalance
? `Insufficient ${fromToken.symbol} Balance`
+ : exchangeRate === 0
+ ? 'No Pool Available'
: 'Swap Tokens'}
@@ -468,6 +662,10 @@ const SwapScreen: React.FC = () => {
{toAmount} {toToken.symbol}
+ Exchange Rate
+ 1 {fromToken.symbol} = {exchangeRate.toFixed(4)} {toToken.symbol}
+
+
Slippage
{slippage}%
@@ -519,8 +717,20 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginBottom: 20,
},
- headerTitle: {
+ backButton: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: '#F5F5F5',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ backButtonText: {
fontSize: 24,
+ color: '#333',
+ },
+ headerTitle: {
+ fontSize: 20,
fontWeight: 'bold',
color: '#333',
},
@@ -693,22 +903,16 @@ const styles = StyleSheet.create({
left: 0,
right: 0,
bottom: 0,
- backgroundColor: 'rgba(0,0,0,0.7)',
+ backgroundColor: 'rgba(0,0,0,0.85)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
- loadingCard: {
- backgroundColor: '#FFFFFF',
- borderRadius: 20,
- padding: 32,
- alignItems: 'center',
- gap: 16,
- },
loadingText: {
- fontSize: 16,
+ marginTop: 24,
+ fontSize: 18,
fontWeight: '600',
- color: '#333',
+ color: '#FFFFFF',
},
modalOverlay: {
flex: 1,
diff --git a/mobile/src/screens/VerifyHumanScreen.tsx b/mobile/src/screens/VerifyHumanScreen.tsx
index 0c8a8048..15a7a5f6 100644
--- a/mobile/src/screens/VerifyHumanScreen.tsx
+++ b/mobile/src/screens/VerifyHumanScreen.tsx
@@ -9,7 +9,6 @@ import {
Animated,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { KurdistanColors } from '../theme/colors';
import AsyncStorage from '@react-native-async-storage/async-storage';
@@ -20,7 +19,6 @@ interface VerifyHumanScreenProps {
}
const VerifyHumanScreen: React.FC = ({ onVerified }) => {
- const { t } = useTranslation();
const [isChecked, setIsChecked] = useState(false);
const [scaleValue] = useState(new Animated.Value(1));
@@ -71,9 +69,9 @@ const VerifyHumanScreen: React.FC = ({ onVerified }) =>
{/* Title */}
- {t('verify.title', 'Security Verification')}
+ Security Verification
- {t('verify.subtitle', 'Please confirm you are human to continue')}
+ Please confirm you are human to continue
{/* Verification Box */}
@@ -86,13 +84,13 @@ const VerifyHumanScreen: React.FC = ({ onVerified }) =>
{isChecked && ✓}
- {t('verify.checkbox', "I'm not a robot")}
+ I'm not a robot
{/* Info Text */}
- {t('verify.info', 'This helps protect the Pezkuwi network from automated attacks')}
+ This helps protect the Pezkuwi network from automated attacks
{/* Continue Button */}
@@ -109,7 +107,7 @@ const VerifyHumanScreen: React.FC = ({ onVerified }) =>
!isChecked && styles.continueButtonTextDisabled,
]}
>
- {t('verify.continue', 'Continue')}
+ Continue
@@ -117,7 +115,7 @@ const VerifyHumanScreen: React.FC = ({ onVerified }) =>
{/* Footer */}
- 🔒 {t('verify.secure', 'Secure & Private')}
+ Secure & Private
diff --git a/mobile/src/screens/WalletScreen.tsx b/mobile/src/screens/WalletScreen.tsx
index a654df3d..282eca28 100644
--- a/mobile/src/screens/WalletScreen.tsx
+++ b/mobile/src/screens/WalletScreen.tsx
@@ -18,12 +18,44 @@ import {
Share,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import QRCode from 'react-native-qrcode-svg';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import * as SecureStore from 'expo-secure-store';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi, NetworkType, NETWORKS } from '../contexts/PezkuwiContext';
import { AddTokenModal } from '../components/wallet/AddTokenModal';
+import { HezTokenLogo, PezTokenLogo } from '../components/icons';
+
+// Secure storage helper - same as in PezkuwiContext
+const secureStorage = {
+ getItem: async (key: string): Promise => {
+ if (Platform.OS === 'web') {
+ return await AsyncStorage.getItem(key);
+ } else {
+ return await SecureStore.getItemAsync(key);
+ }
+ },
+};
+
+// Cross-platform alert helper
+const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void; style?: string}>) => {
+ if (Platform.OS === 'web') {
+ if (buttons && buttons.length > 1) {
+ const result = window.confirm(`${title}\n\n${message}`);
+ if (result && buttons[1]?.onPress) {
+ buttons[1].onPress();
+ } else if (!result && buttons[0]?.onPress) {
+ buttons[0].onPress();
+ }
+ } else {
+ window.alert(`${title}\n\n${message}`);
+ if (buttons?.[0]?.onPress) buttons[0].onPress();
+ }
+ } else {
+ showAlert(title, message, buttons as any);
+ }
+};
// Token Images - From shared/images
const hezLogo = require('../../../shared/images/hez_logo.png');
@@ -59,16 +91,17 @@ interface Transaction {
}
const WalletScreen: React.FC = () => {
- const { t } = useTranslation();
const navigation = useNavigation();
const {
api,
isApiReady,
accounts,
selectedAccount,
+ setSelectedAccount,
connectWallet,
disconnectWallet,
createWallet,
+ deleteWallet,
getKeyPair,
currentNetwork,
switchNetwork,
@@ -82,6 +115,7 @@ const WalletScreen: React.FC = () => {
const [importWalletModalVisible, setImportWalletModalVisible] = useState(false);
const [backupModalVisible, setBackupModalVisible] = useState(false);
const [networkSelectorVisible, setNetworkSelectorVisible] = useState(false);
+ const [walletSelectorVisible, setWalletSelectorVisible] = useState(false);
const [addTokenModalVisible, setAddTokenModalVisible] = useState(false);
const [recipientAddress, setRecipientAddress] = useState('');
const [sendAmount, setSendAmount] = useState('');
@@ -225,7 +259,7 @@ const WalletScreen: React.FC = () => {
const handleConfirmSend = async () => {
if (!recipientAddress || !sendAmount || !selectedToken || !selectedAccount || !api) {
- Alert.alert('Error', 'Please enter recipient address and amount');
+ showAlert('Error', 'Please enter recipient address and amount');
return;
}
@@ -251,13 +285,13 @@ const WalletScreen: React.FC = () => {
if (status.isFinalized) {
setSendModalVisible(false);
setIsSending(false);
- Alert.alert('Success', 'Transaction Sent!');
+ showAlert('Success', 'Transaction Sent!');
fetchData();
}
});
} catch (e: any) {
setIsSending(false);
- Alert.alert('Error', e.message);
+ showAlert('Error', e.message);
}
};
@@ -272,21 +306,21 @@ const WalletScreen: React.FC = () => {
const { address, mnemonic } = await createWallet(walletName);
setUserMnemonic(mnemonic); // Save for backup
setCreateWalletModalVisible(false);
- Alert.alert('Wallet Created', `Save this mnemonic:\n${mnemonic}`, [{ text: 'OK', onPress: () => connectWallet() }]);
- } catch (e) { Alert.alert('Error', 'Failed'); }
+ showAlert('Wallet Created', `Save this mnemonic:\n${mnemonic}`, [{ text: 'OK', onPress: () => connectWallet() }]);
+ } catch (e) { showAlert('Error', 'Failed'); }
};
// Copy Address Handler
const handleCopyAddress = () => {
if (!selectedAccount) return;
Clipboard.setString(selectedAccount.address);
- Alert.alert('Copied!', 'Address copied to clipboard');
+ showAlert('Copied!', 'Address copied to clipboard');
};
// Import Wallet Handler
const handleImportWallet = async () => {
if (!importMnemonic.trim()) {
- Alert.alert('Error', 'Please enter a valid mnemonic');
+ showAlert('Error', 'Please enter a valid mnemonic');
return;
}
try {
@@ -297,23 +331,36 @@ const WalletScreen: React.FC = () => {
const pair = keyring.addFromMnemonic(importMnemonic.trim());
// Store in AsyncStorage (via context method ideally)
- Alert.alert('Success', `Wallet imported: ${pair.address.slice(0,8)}...`);
+ showAlert('Success', `Wallet imported: ${pair.address.slice(0,8)}...`);
setImportWalletModalVisible(false);
setImportMnemonic('');
connectWallet();
} catch (e: any) {
- Alert.alert('Error', e.message || 'Invalid mnemonic');
+ showAlert('Error', e.message || 'Invalid mnemonic');
}
};
// Backup Mnemonic Handler
const handleBackupMnemonic = async () => {
- // Retrieve mnemonic from secure storage
- // For demo, we show the saved one or prompt user
- if (userMnemonic) {
- setBackupModalVisible(true);
- } else {
- Alert.alert('No Backup', 'Mnemonic not available. Create a new wallet or import existing one.');
+ 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}`;
+ const storedMnemonic = await secureStorage.getItem(seedKey);
+
+ if (storedMnemonic) {
+ setUserMnemonic(storedMnemonic);
+ setBackupModalVisible(true);
+ } else {
+ showAlert('No Backup', 'Mnemonic not found in secure storage. It may have been imported from another device.');
+ }
+ } catch (error) {
+ console.error('Error retrieving mnemonic:', error);
+ showAlert('Error', 'Failed to retrieve mnemonic from secure storage.');
}
};
@@ -334,156 +381,110 @@ const WalletScreen: React.FC = () => {
try {
await switchNetwork(network);
setNetworkSelectorVisible(false);
- Alert.alert('Success', `Switched to ${NETWORKS[network].displayName}`);
+ showAlert('Success', `Switched to ${NETWORKS[network].displayName}`);
} catch (e: any) {
- Alert.alert('Error', e.message || 'Failed to switch network');
+ showAlert('Error', e.message || 'Failed to switch network');
}
};
- if (!selectedAccount) {
- return (
-
-
-
- 🔐
- Welcome to
- Pezkuwichain Wallet
-
- Secure, Fast & Decentralized
-
-
-
+ // Redirect to WalletSetupScreen if no wallet exists
+ useEffect(() => {
+ if (!selectedAccount && accounts.length === 0) {
+ navigation.replace('WalletSetup');
+ }
+ }, [selectedAccount, accounts, navigation]);
-
-
-
-
- ➕
-
-
- Create New Wallet
-
- Get started in seconds
-
-
-
-
-
- setImportWalletModalVisible(true)}
- activeOpacity={0.8}
- >
-
-
- 📥
-
-
- Import Existing Wallet
-
- Use your seed phrase
-
-
-
-
-
-
- 🛡️
-
- Your keys are encrypted and stored locally on your device
-
-
-
-
- {/* Create Wallet Modal */}
- setCreateWalletModalVisible(false)}>
-
-
- Create New Wallet
-
-
- setCreateWalletModalVisible(false)}>Cancel
- Create
-
-
-
-
-
- {/* Import Wallet Modal */}
- setImportWalletModalVisible(false)}>
-
-
- Import Wallet
- Enter your 12 or 24 word mnemonic phrase
-
-
- setImportWalletModalVisible(false)}>Cancel
- Import
-
-
-
-
-
- );
+ // Show loading while checking wallet state or redirecting
+ if (!selectedAccount && accounts.length === 0) {
+ return (
+
+
+
+ Loading wallet...
+
+
+ );
}
return (
-
+
+ {/* Top Header with Back Button */}
+
+ navigation.goBack()} style={styles.backButton} testID="wallet-back-button">
+ ← Back
+
+ Wallet
+ setNetworkSelectorVisible(true)} testID="wallet-network-button">
+ 🌐 {NETWORKS[currentNetwork].displayName}
+
+
+
}
showsVerticalScrollIndicator={false}
+ testID="wallet-scroll-view"
>
- {/* Header */}
-
- pezkuwi wallet
- setNetworkSelectorVisible(true)}>
- 🌐 {NETWORKS[currentNetwork].displayName}
+ {/* Wallet Selector Row */}
+
+ setWalletSelectorVisible(true)}
+ testID="wallet-selector-button"
+ >
+
+ {selectedAccount?.name || 'Wallet'}
+
+ {selectedAccount?.address ? `${selectedAccount.address.slice(0, 8)}...${selectedAccount.address.slice(-6)}` : ''}
+
+
+ ▼
+
+ navigation.navigate('WalletSetup')}
+ testID="add-wallet-button"
+ >
+ +
+
+ showAlert('Scan', 'QR Scanner coming soon')}
+ testID="wallet-scan-button"
+ >
+ ⊡
+
+
{/* Main Token Cards - HEZ and PEZ side by side */}
{/* HEZ Card */}
handleTokenPress(tokens[0])}>
-
+
+
+
HEZ
{balances.HEZ}
- Hemuwelet Token
+ Welati Coin
{/* PEZ Card */}
handleTokenPress(tokens[1])}>
-
+
+
+
PEZ
{balances.PEZ}
- Pezkunel Token
+ Pezkuwichain Token
- {/* Action Buttons Grid - 2x4 */}
+ {/* Action Buttons Grid - 1x4 */}
- {/* Row 1 */}
↑
Send
@@ -494,36 +495,15 @@ const WalletScreen: React.FC = () => {
Receive
- Alert.alert('Scan', 'QR Scanner coming soon')}>
- ⊡
- Scan
+ navigation.navigate('Swap')}>
+ 🔄
+ Swap
- Alert.alert('P2P', 'Navigate to P2P Platform')}>
- 👥
- P2P
-
-
- {/* Row 2 */}
- Alert.alert('Vote', 'Navigate to Governance')}>
- 🗳️
- Vote
-
-
- Alert.alert('Dapps', 'Navigate to Apps')}>
- ⊞
- Dapps
-
-
- Alert.alert('Staking', 'Navigate to Staking')}>
+ showAlert('Staking', 'Navigate to Staking')}>
🥩
Staking
-
- setNetworkSelectorVisible(true)}>
- 🔗
- Connect
-
{/* Tokens List */}
@@ -630,7 +610,7 @@ const WalletScreen: React.FC = () => {
{
Clipboard.setString(userMnemonic);
- Alert.alert('Copied', 'Mnemonic copied to clipboard');
+ showAlert('Copied', 'Mnemonic copied to clipboard');
}}>
📋 Copy
@@ -642,6 +622,103 @@ const WalletScreen: React.FC = () => {
+ {/* Wallet Selector Modal */}
+ setWalletSelectorVisible(false)}>
+
+
+ 👛 My Wallets
+
+ Select a wallet or create a new one
+
+
+ {/* Wallet List */}
+ {accounts.map((account) => {
+ const isSelected = account.address === selectedAccount?.address;
+ return (
+
+ {
+ setSelectedAccount(account);
+ setWalletSelectorVisible(false);
+ }}
+ >
+
+ 👛
+
+
+
+ {account.name}
+
+
+ {account.address.slice(0, 12)}...{account.address.slice(-8)}
+
+
+ {isSelected && ✓}
+
+ {
+ const confirmDelete = Platform.OS === 'web'
+ ? window.confirm(`Delete "${account.name}"?\n\nThis action cannot be undone. Make sure you have backed up your recovery phrase.`)
+ : await new Promise((resolve) => {
+ Alert.alert(
+ 'Delete Wallet',
+ `Are you sure you want to delete "${account.name}"?\n\nThis action cannot be undone. Make sure you have backed up your recovery phrase.`,
+ [
+ { text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
+ { text: 'Delete', style: 'destructive', onPress: () => resolve(true) }
+ ]
+ );
+ });
+
+ if (confirmDelete) {
+ try {
+ await deleteWallet(account.address);
+ if (accounts.length <= 1) {
+ setWalletSelectorVisible(false);
+ }
+ } catch (err) {
+ if (Platform.OS === 'web') {
+ window.alert('Failed to delete wallet');
+ } else {
+ Alert.alert('Error', 'Failed to delete wallet');
+ }
+ }
+ }
+ }}
+ >
+ 🗑️
+
+
+ );
+ })}
+
+ {/* Add New Wallet Button */}
+ {
+ setWalletSelectorVisible(false);
+ navigation.navigate('WalletSetup');
+ }}
+ >
+
+ +
+
+ Add New Wallet
+
+
+ setWalletSelectorVisible(false)}>
+ Close
+
+
+
+
+
{/* Network Selector Modal */}
setNetworkSelectorVisible(false)}>
@@ -700,6 +777,42 @@ const styles = StyleSheet.create({
scrollContent: {
flex: 1,
},
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingText: {
+ marginTop: 16,
+ fontSize: 16,
+ color: '#666',
+ },
+
+ // Top Header with Back Button
+ topHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ backgroundColor: '#FFFFFF',
+ borderBottomWidth: 1,
+ borderBottomColor: '#E5E5E5',
+ },
+ backButton: {
+ paddingVertical: 4,
+ paddingRight: 8,
+ },
+ backButtonText: {
+ fontSize: 16,
+ color: KurdistanColors.kesk,
+ fontWeight: '500',
+ },
+ topHeaderTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#333',
+ },
// Header Styles (New Design)
headerContainer: {
@@ -725,6 +838,18 @@ const styles = StyleSheet.create({
borderRadius: 16,
overflow: 'hidden',
},
+ scanButton: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: '#F5F5F5',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ scanIcon: {
+ fontSize: 20,
+ color: '#333',
+ },
// Main Token Cards (HEZ & PEZ) - New Design
mainTokensRow: {
@@ -747,6 +872,13 @@ const styles = StyleSheet.create({
height: 56,
marginBottom: 12,
},
+ mainTokenLogoContainer: {
+ width: 56,
+ height: 56,
+ marginBottom: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
mainTokenSymbol: {
fontSize: 18,
fontWeight: 'bold',
@@ -1071,6 +1203,140 @@ const styles = StyleSheet.create({
color: '#666',
lineHeight: 18,
},
+ // Wallet Selector Row
+ walletSelectorRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ marginBottom: 8,
+ },
+ walletSelector: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: '#FFFFFF',
+ borderRadius: 12,
+ padding: 12,
+ marginRight: 12,
+ boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.05)',
+ elevation: 2,
+ },
+ walletSelectorInfo: {
+ flex: 1,
+ },
+ walletSelectorName: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: KurdistanColors.reş,
+ },
+ walletSelectorAddress: {
+ fontSize: 12,
+ color: '#999',
+ marginTop: 2,
+ },
+ walletSelectorArrow: {
+ fontSize: 12,
+ color: '#666',
+ marginLeft: 8,
+ },
+ walletHeaderButtons: {
+ flexDirection: 'row',
+ gap: 8,
+ },
+ addWalletButton: {
+ width: 44,
+ height: 44,
+ borderRadius: 12,
+ backgroundColor: '#FFFFFF',
+ justifyContent: 'center',
+ alignItems: 'center',
+ boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.05)',
+ elevation: 2,
+ },
+ addWalletIcon: {
+ fontSize: 24,
+ color: KurdistanColors.kesk,
+ fontWeight: '300',
+ },
+ // Wallet Selector Modal
+ walletOption: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ backgroundColor: '#F8F9FA',
+ borderRadius: 12,
+ marginBottom: 8,
+ borderWidth: 2,
+ borderColor: 'transparent',
+ },
+ walletOptionSelected: {
+ borderColor: KurdistanColors.kesk,
+ backgroundColor: 'rgba(0, 143, 67, 0.05)',
+ },
+ walletOptionIcon: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ backgroundColor: '#FFFFFF',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ walletOptionName: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: KurdistanColors.reş,
+ },
+ walletOptionAddress: {
+ fontSize: 12,
+ color: '#999',
+ marginTop: 2,
+ },
+ addNewWalletOption: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ backgroundColor: 'rgba(0, 143, 67, 0.05)',
+ borderRadius: 12,
+ marginBottom: 16,
+ borderWidth: 2,
+ borderColor: KurdistanColors.kesk,
+ borderStyle: 'dashed',
+ },
+ addNewWalletIcon: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ backgroundColor: '#FFFFFF',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ addNewWalletText: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: KurdistanColors.kesk,
+ },
+ // Delete wallet
+ walletOptionRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ gap: 8,
+ },
+ deleteWalletButton: {
+ width: 44,
+ height: 44,
+ borderRadius: 12,
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ deleteWalletIcon: {
+ fontSize: 18,
+ },
});
export default WalletScreen;
\ No newline at end of file
diff --git a/mobile/src/screens/WalletSetupScreen.tsx b/mobile/src/screens/WalletSetupScreen.tsx
new file mode 100644
index 00000000..d8ee1bce
--- /dev/null
+++ b/mobile/src/screens/WalletSetupScreen.tsx
@@ -0,0 +1,820 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ StyleSheet,
+ SafeAreaView,
+ ScrollView,
+ TextInput,
+ ActivityIndicator,
+ Alert,
+ Platform,
+} from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { LinearGradient } from 'expo-linear-gradient';
+import { KurdistanColors } from '../theme/colors';
+import { usePezkuwi } from '../contexts/PezkuwiContext';
+import { mnemonicGenerate, mnemonicValidate } from '@pezkuwi/util-crypto';
+
+// Cross-platform alert helper
+const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void; style?: string}>) => {
+ if (Platform.OS === 'web') {
+ window.alert(`${title}\n\n${message}`);
+ if (buttons?.[0]?.onPress) buttons[0].onPress();
+ } else {
+ Alert.alert(title, message, buttons as any);
+ }
+};
+
+type SetupStep = 'choice' | 'create-show' | 'create-verify' | 'import' | 'wallet-name' | 'success';
+
+const WalletSetupScreen: React.FC = () => {
+ const navigation = useNavigation();
+ const { createWallet, importWallet, connectWallet, isReady } = usePezkuwi();
+
+ const [step, setStep] = useState('choice');
+ const [mnemonic, setMnemonic] = useState([]);
+ const [walletName, setWalletName] = useState('');
+ const [importMnemonic, setImportMnemonic] = useState('');
+ const [verificationIndices, setVerificationIndices] = useState([]);
+ const [selectedWords, setSelectedWords] = useState<{[key: number]: string}>({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [createdAddress, setCreatedAddress] = useState('');
+ const [isCreateFlow, setIsCreateFlow] = useState(true);
+
+ // Generate mnemonic when entering create flow
+ const handleCreateNew = () => {
+ const generatedMnemonic = mnemonicGenerate(12);
+ setMnemonic(generatedMnemonic.split(' '));
+ setIsCreateFlow(true);
+ setStep('create-show');
+ };
+
+ // Go to import flow
+ const handleImport = () => {
+ setIsCreateFlow(false);
+ setStep('import');
+ };
+
+ // After showing mnemonic, go to verification
+ const handleMnemonicConfirmed = () => {
+ // Select 3 random indices for verification
+ const indices: number[] = [];
+ while (indices.length < 3) {
+ const randomIndex = Math.floor(Math.random() * 12);
+ if (!indices.includes(randomIndex)) {
+ indices.push(randomIndex);
+ }
+ }
+ indices.sort((a, b) => a - b);
+ setVerificationIndices(indices);
+ setSelectedWords({});
+ setStep('create-verify');
+ };
+
+ // Verify selected words
+ const handleVerifyWord = (index: number, word: string) => {
+ setSelectedWords(prev => ({ ...prev, [index]: word }));
+ };
+
+ // Check if verification is complete and correct
+ const isVerificationComplete = () => {
+ return verificationIndices.every(idx => selectedWords[idx] === mnemonic[idx]);
+ };
+
+ // After verification, go to wallet name
+ const handleVerificationComplete = () => {
+ if (!isVerificationComplete()) {
+ showAlert('Incorrect', 'The words you selected do not match. Please try again.');
+ setSelectedWords({});
+ return;
+ }
+ setStep('wallet-name');
+ };
+
+ // Create wallet with name
+ const handleCreateWallet = async () => {
+ if (!walletName.trim()) {
+ showAlert('Error', 'Please enter a wallet name');
+ return;
+ }
+
+ if (!isReady) {
+ showAlert('Error', 'Crypto libraries are still loading. Please wait a moment and try again.');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const { address } = await createWallet(walletName.trim(), mnemonic.join(' '));
+ setCreatedAddress(address);
+ await connectWallet();
+ setStep('success');
+ } catch (error: any) {
+ console.error('[WalletSetup] Create wallet error:', error);
+ showAlert('Error', error.message || 'Failed to create wallet');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Import wallet with mnemonic or dev URI (like //Alice)
+ const handleImportWallet = async () => {
+ const trimmedInput = importMnemonic.trim();
+
+ // Check if it's a dev URI (starts with //)
+ if (trimmedInput.startsWith('//')) {
+ // Dev URI like //Alice, //Bob, etc.
+ setMnemonic([trimmedInput]); // Store as single-element array to indicate URI
+ setStep('wallet-name');
+ return;
+ }
+
+ // Otherwise treat as mnemonic
+ const words = trimmedInput.toLowerCase().split(/\s+/);
+
+ if (words.length !== 12 && words.length !== 24) {
+ showAlert('Invalid Input', 'Please enter a valid 12 or 24 word recovery phrase, or a dev URI like //Alice');
+ return;
+ }
+
+ if (!mnemonicValidate(trimmedInput.toLowerCase())) {
+ showAlert('Invalid Mnemonic', 'The recovery phrase is invalid. Please check and try again.');
+ return;
+ }
+
+ setMnemonic(words);
+ setStep('wallet-name');
+ };
+
+ // After naming imported wallet
+ const handleImportComplete = async () => {
+ if (!walletName.trim()) {
+ showAlert('Error', 'Please enter a wallet name');
+ return;
+ }
+
+ if (!isReady) {
+ showAlert('Error', 'Crypto libraries are still loading. Please wait a moment and try again.');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const { address } = await importWallet(walletName.trim(), mnemonic.join(' '));
+ setCreatedAddress(address);
+ await connectWallet();
+ setStep('success');
+ } catch (error: any) {
+ console.error('[WalletSetup] Import wallet error:', error);
+ showAlert('Error', error.message || 'Failed to import wallet');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Finish setup and go to wallet
+ const handleFinish = () => {
+ navigation.replace('Wallet');
+ };
+
+ // Go back to previous step
+ const handleBack = () => {
+ switch (step) {
+ case 'create-show':
+ case 'import':
+ setStep('choice');
+ break;
+ case 'create-verify':
+ setStep('create-show');
+ break;
+ case 'wallet-name':
+ if (isCreateFlow) {
+ setStep('create-verify');
+ } else {
+ setStep('import');
+ }
+ break;
+ default:
+ navigation.goBack();
+ }
+ };
+
+ // Generate shuffled words for verification options
+ const getShuffledOptions = (correctWord: string): string[] => {
+ const allWords = [...mnemonic];
+ const options = [correctWord];
+
+ while (options.length < 4) {
+ const randomWord = allWords[Math.floor(Math.random() * allWords.length)];
+ if (!options.includes(randomWord)) {
+ options.push(randomWord);
+ }
+ }
+
+ // Shuffle options
+ return options.sort(() => Math.random() - 0.5);
+ };
+
+ // ============================================================
+ // RENDER FUNCTIONS
+ // ============================================================
+
+ const renderChoiceStep = () => (
+
+
+ 👛
+
+
+ Set Up Your Wallet
+
+ Create a new wallet or import an existing one using your recovery phrase
+
+
+
+
+
+ ✨
+ Create New Wallet
+
+ Generate a new recovery phrase
+
+
+
+
+
+
+ 📥
+
+ Import Existing Wallet
+
+
+ Use your 12 or 24 word phrase
+
+
+
+
+
+ );
+
+ const renderCreateShowStep = () => (
+
+ Your Recovery Phrase
+
+ Write down these 12 words in order and keep them safe. This is the only way to recover your wallet.
+
+
+
+ ⚠️
+
+ Never share your recovery phrase with anyone. Anyone with these words can access your funds.
+
+
+
+
+ {mnemonic.map((word, index) => (
+
+ {index + 1}
+ {word}
+
+ ))}
+
+
+
+ I've Written It Down
+
+
+ );
+
+ const renderCreateVerifyStep = () => (
+
+ Verify Your Phrase
+
+ Select the correct words to verify you've saved your recovery phrase
+
+
+
+ {verificationIndices.map((wordIndex) => (
+
+ Word #{wordIndex + 1}
+
+ {getShuffledOptions(mnemonic[wordIndex]).map((option, optIdx) => (
+ handleVerifyWord(wordIndex, option)}
+ testID={`verify-option-${wordIndex}-${optIdx}`}
+ >
+
+ {option}
+
+
+ ))}
+
+
+ ))}
+
+
+
+ Verify & Continue
+
+
+ );
+
+ const renderImportStep = () => (
+
+ Import Wallet
+
+ Enter your 12 or 24 word recovery phrase, or a dev URI like //Alice
+
+
+
+
+
+ Mnemonic: separate words with space | Dev URI: //Alice, //Bob, etc.
+
+
+
+
+ Continue
+
+
+ );
+
+ const renderWalletNameStep = () => (
+
+ Name Your Wallet
+
+ Give your wallet a name to easily identify it
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ {isCreateFlow ? 'Create Wallet' : 'Import Wallet'}
+
+ )}
+
+
+ );
+
+ const renderSuccessStep = () => (
+
+
+ ✅
+
+
+ Wallet Created!
+
+ Your wallet is ready to use. You can now send and receive tokens.
+
+
+
+ Your Wallet Address
+
+ {createdAddress}
+
+
+
+
+ Go to Wallet
+
+
+ );
+
+ const renderStep = () => {
+ switch (step) {
+ case 'choice':
+ return renderChoiceStep();
+ case 'create-show':
+ return renderCreateShowStep();
+ case 'create-verify':
+ return renderCreateVerifyStep();
+ case 'import':
+ return renderImportStep();
+ case 'wallet-name':
+ return renderWalletNameStep();
+ case 'success':
+ return renderSuccessStep();
+ default:
+ return renderChoiceStep();
+ }
+ };
+
+ return (
+
+ {/* Header */}
+ {step !== 'choice' && step !== 'success' && (
+
+
+ ← Back
+
+
+ {['create-show', 'create-verify', 'wallet-name'].includes(step) && isCreateFlow && (
+ <>
+
+
+
+ >
+ )}
+ {['import', 'wallet-name'].includes(step) && !isCreateFlow && (
+ <>
+
+
+ >
+ )}
+
+
+
+ )}
+
+ {/* Close button on choice screen */}
+ {step === 'choice' && (
+
+ navigation.goBack()} style={styles.closeButton} testID="wallet-setup-close">
+ ✕
+
+
+ )}
+
+
+ {renderStep()}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#FFFFFF',
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: '#F0F0F0',
+ },
+ backButton: {
+ paddingVertical: 4,
+ paddingRight: 16,
+ },
+ backButtonText: {
+ fontSize: 16,
+ color: KurdistanColors.kesk,
+ fontWeight: '500',
+ },
+ closeButton: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: '#F5F5F5',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ closeButtonText: {
+ fontSize: 18,
+ color: '#666',
+ },
+ progressContainer: {
+ flexDirection: 'row',
+ gap: 8,
+ },
+ progressDot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: '#E0E0E0',
+ },
+ progressDotActive: {
+ backgroundColor: KurdistanColors.kesk,
+ },
+ headerSpacer: {
+ width: 60,
+ },
+ content: {
+ flex: 1,
+ },
+ contentContainer: {
+ padding: 24,
+ paddingBottom: 40,
+ },
+ stepContainer: {
+ flex: 1,
+ },
+ iconContainer: {
+ alignItems: 'center',
+ marginBottom: 24,
+ marginTop: 20,
+ },
+ mainIcon: {
+ fontSize: 80,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ color: KurdistanColors.reş,
+ textAlign: 'center',
+ marginBottom: 12,
+ },
+ subtitle: {
+ fontSize: 16,
+ color: '#666',
+ textAlign: 'center',
+ lineHeight: 24,
+ marginBottom: 32,
+ },
+
+ // Choice buttons
+ choiceButtons: {
+ gap: 16,
+ },
+ choiceButton: {
+ borderRadius: 16,
+ overflow: 'hidden',
+ },
+ choiceButtonGradient: {
+ padding: 24,
+ alignItems: 'center',
+ },
+ choiceButtonOutline: {
+ padding: 24,
+ alignItems: 'center',
+ borderWidth: 2,
+ borderColor: '#E0E0E0',
+ borderRadius: 16,
+ },
+ choiceButtonIcon: {
+ fontSize: 40,
+ marginBottom: 12,
+ },
+ choiceButtonTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#FFFFFF',
+ marginBottom: 4,
+ },
+ choiceButtonSubtitle: {
+ fontSize: 14,
+ color: 'rgba(255,255,255,0.8)',
+ },
+
+ // Warning box
+ warningBox: {
+ flexDirection: 'row',
+ backgroundColor: '#FFF3CD',
+ borderRadius: 12,
+ padding: 16,
+ marginBottom: 24,
+ alignItems: 'flex-start',
+ },
+ warningIcon: {
+ fontSize: 20,
+ marginRight: 12,
+ },
+ warningText: {
+ flex: 1,
+ fontSize: 14,
+ color: '#856404',
+ lineHeight: 20,
+ },
+
+ // Mnemonic grid
+ mnemonicGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'space-between',
+ marginBottom: 32,
+ },
+ wordCard: {
+ width: '31%',
+ backgroundColor: '#F8F9FA',
+ borderRadius: 12,
+ padding: 12,
+ marginBottom: 12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ wordNumber: {
+ fontSize: 12,
+ color: '#999',
+ marginRight: 8,
+ minWidth: 20,
+ },
+ wordText: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: KurdistanColors.reş,
+ },
+
+ // Verification
+ verificationContainer: {
+ marginBottom: 32,
+ },
+ verificationItem: {
+ marginBottom: 24,
+ },
+ verificationLabel: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: KurdistanColors.reş,
+ marginBottom: 12,
+ },
+ verificationOptions: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 10,
+ },
+ verificationOption: {
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ borderRadius: 20,
+ backgroundColor: '#F5F5F5',
+ borderWidth: 2,
+ borderColor: 'transparent',
+ },
+ verificationOptionSelected: {
+ borderColor: KurdistanColors.kesk,
+ backgroundColor: 'rgba(0, 143, 67, 0.1)',
+ },
+ verificationOptionCorrect: {
+ borderColor: KurdistanColors.kesk,
+ backgroundColor: 'rgba(0, 143, 67, 0.15)',
+ },
+ verificationOptionText: {
+ fontSize: 14,
+ color: '#333',
+ },
+ verificationOptionTextSelected: {
+ color: KurdistanColors.kesk,
+ fontWeight: '600',
+ },
+
+ // Import
+ importInputContainer: {
+ marginBottom: 32,
+ },
+ importInput: {
+ backgroundColor: '#F8F9FA',
+ borderRadius: 16,
+ padding: 16,
+ fontSize: 16,
+ color: KurdistanColors.reş,
+ minHeight: 120,
+ textAlignVertical: 'top',
+ borderWidth: 1,
+ borderColor: '#E0E0E0',
+ },
+ importHint: {
+ fontSize: 12,
+ color: '#999',
+ marginTop: 8,
+ marginLeft: 4,
+ },
+
+ // Wallet name
+ nameInputContainer: {
+ marginBottom: 32,
+ },
+ nameInput: {
+ backgroundColor: '#F8F9FA',
+ borderRadius: 16,
+ padding: 16,
+ fontSize: 18,
+ color: KurdistanColors.reş,
+ borderWidth: 1,
+ borderColor: '#E0E0E0',
+ textAlign: 'center',
+ },
+
+ // Primary button
+ primaryButton: {
+ backgroundColor: KurdistanColors.kesk,
+ borderRadius: 16,
+ padding: 18,
+ alignItems: 'center',
+ },
+ primaryButtonDisabled: {
+ opacity: 0.5,
+ },
+ primaryButtonText: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#FFFFFF',
+ },
+
+ // Success
+ successIconContainer: {
+ alignItems: 'center',
+ marginBottom: 24,
+ marginTop: 40,
+ },
+ successIcon: {
+ fontSize: 80,
+ },
+ addressBox: {
+ backgroundColor: '#F8F9FA',
+ borderRadius: 16,
+ padding: 20,
+ marginBottom: 32,
+ alignItems: 'center',
+ },
+ addressLabel: {
+ fontSize: 12,
+ color: '#999',
+ marginBottom: 8,
+ },
+ addressText: {
+ fontSize: 14,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ color: KurdistanColors.reş,
+ },
+});
+
+export default WalletSetupScreen;
diff --git a/mobile/src/screens/WelcomeScreen.tsx b/mobile/src/screens/WelcomeScreen.tsx
index 5e104349..fe52b03c 100644
--- a/mobile/src/screens/WelcomeScreen.tsx
+++ b/mobile/src/screens/WelcomeScreen.tsx
@@ -10,7 +10,6 @@ import {
Image,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { useTranslation } from 'react-i18next';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { KurdistanColors } from '../theme/colors';
import PrivacyPolicyModal from '../components/PrivacyPolicyModal';
@@ -21,7 +20,6 @@ interface WelcomeScreenProps {
}
const WelcomeScreen: React.FC = ({ onContinue }) => {
- const { t } = useTranslation();
const [agreed, setAgreed] = useState(false);
const [privacyModalVisible, setPrivacyModalVisible] = useState(false);
const [termsModalVisible, setTermsModalVisible] = useState(false);
diff --git a/mobile/src/screens/__tests__/BeCitizenScreen.test.tsx b/mobile/src/screens/__tests__/BeCitizenScreen.test.tsx
index 7e61d83c..2c53bfaa 100644
--- a/mobile/src/screens/__tests__/BeCitizenScreen.test.tsx
+++ b/mobile/src/screens/__tests__/BeCitizenScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import BeCitizenScreen from '../BeCitizenScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const BeCitizenScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('BeCitizenScreen', () => {
diff --git a/mobile/src/screens/__tests__/EducationScreen.test.tsx b/mobile/src/screens/__tests__/EducationScreen.test.tsx
index dd0afdb0..dc2d163e 100644
--- a/mobile/src/screens/__tests__/EducationScreen.test.tsx
+++ b/mobile/src/screens/__tests__/EducationScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import EducationScreen from '../EducationScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const EducationScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('EducationScreen', () => {
diff --git a/mobile/src/screens/__tests__/ForumScreen.test.tsx b/mobile/src/screens/__tests__/ForumScreen.test.tsx
index 2df198ba..5ae16490 100644
--- a/mobile/src/screens/__tests__/ForumScreen.test.tsx
+++ b/mobile/src/screens/__tests__/ForumScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { AuthProvider } from '../../contexts/AuthContext';
import ForumScreen from '../ForumScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const ForumScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('ForumScreen', () => {
diff --git a/mobile/src/screens/__tests__/GovernanceScreen.test.tsx b/mobile/src/screens/__tests__/GovernanceScreen.test.tsx
index 787659be..7062eeb4 100644
--- a/mobile/src/screens/__tests__/GovernanceScreen.test.tsx
+++ b/mobile/src/screens/__tests__/GovernanceScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import GovernanceScreen from '../GovernanceScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const GovernanceScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('GovernanceScreen', () => {
diff --git a/mobile/src/screens/__tests__/LockScreen.test.tsx b/mobile/src/screens/__tests__/LockScreen.test.tsx
index 20ef4b77..f1a02a66 100644
--- a/mobile/src/screens/__tests__/LockScreen.test.tsx
+++ b/mobile/src/screens/__tests__/LockScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { BiometricAuthProvider } from '../../contexts/BiometricAuthContext';
import LockScreen from '../LockScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const LockScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('LockScreen', () => {
diff --git a/mobile/src/screens/__tests__/NFTGalleryScreen.test.tsx b/mobile/src/screens/__tests__/NFTGalleryScreen.test.tsx
index 4f478f80..14fc32c8 100644
--- a/mobile/src/screens/__tests__/NFTGalleryScreen.test.tsx
+++ b/mobile/src/screens/__tests__/NFTGalleryScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import NFTGalleryScreen from '../NFTGalleryScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const NFTGalleryScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('NFTGalleryScreen', () => {
diff --git a/mobile/src/screens/__tests__/P2PScreen.test.tsx b/mobile/src/screens/__tests__/P2PScreen.test.tsx
index 5f8a592a..998cf3b7 100644
--- a/mobile/src/screens/__tests__/P2PScreen.test.tsx
+++ b/mobile/src/screens/__tests__/P2PScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import P2PScreen from '../P2PScreen';
@@ -11,11 +10,9 @@ jest.mock('@react-navigation/native', () => ({
// Wrapper with required providers
const P2PScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('P2PScreen', () => {
diff --git a/mobile/src/screens/__tests__/ProfileScreen.test.tsx b/mobile/src/screens/__tests__/ProfileScreen.test.tsx
index 4a04a1b4..85a890b5 100644
--- a/mobile/src/screens/__tests__/ProfileScreen.test.tsx
+++ b/mobile/src/screens/__tests__/ProfileScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import ProfileScreen from '../ProfileScreen';
jest.mock('@react-navigation/native', () => ({
@@ -9,9 +8,7 @@ jest.mock('@react-navigation/native', () => ({
}));
const ProfileScreenWrapper = () => (
-
-
-
+
);
describe('ProfileScreen', () => {
diff --git a/mobile/src/screens/__tests__/ReferralScreen.test.tsx b/mobile/src/screens/__tests__/ReferralScreen.test.tsx
index 148103b3..54dff207 100644
--- a/mobile/src/screens/__tests__/ReferralScreen.test.tsx
+++ b/mobile/src/screens/__tests__/ReferralScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import ReferralScreen from '../ReferralScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const ReferralScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('ReferralScreen', () => {
diff --git a/mobile/src/screens/__tests__/SecurityScreen.test.tsx b/mobile/src/screens/__tests__/SecurityScreen.test.tsx
index 7842e8d2..a146d4ef 100644
--- a/mobile/src/screens/__tests__/SecurityScreen.test.tsx
+++ b/mobile/src/screens/__tests__/SecurityScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { BiometricAuthProvider } from '../../contexts/BiometricAuthContext';
import SecurityScreen from '../SecurityScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const SecurityScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('SecurityScreen', () => {
diff --git a/mobile/src/screens/__tests__/SignInScreen.test.tsx b/mobile/src/screens/__tests__/SignInScreen.test.tsx
index 599932f9..d77ea08c 100644
--- a/mobile/src/screens/__tests__/SignInScreen.test.tsx
+++ b/mobile/src/screens/__tests__/SignInScreen.test.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { AuthProvider } from '../../contexts/AuthContext';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import SignInScreen from '../SignInScreen';
jest.mock('@react-navigation/native', () => ({
@@ -11,11 +10,9 @@ jest.mock('@react-navigation/native', () => ({
// Wrapper with required providers
const SignInScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('SignInScreen', () => {
diff --git a/mobile/src/screens/__tests__/SignUpScreen.test.tsx b/mobile/src/screens/__tests__/SignUpScreen.test.tsx
index b04d480d..82728d29 100644
--- a/mobile/src/screens/__tests__/SignUpScreen.test.tsx
+++ b/mobile/src/screens/__tests__/SignUpScreen.test.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { AuthProvider } from '../../contexts/AuthContext';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import SignUpScreen from '../SignUpScreen';
jest.mock('@react-navigation/native', () => ({
@@ -11,11 +10,9 @@ jest.mock('@react-navigation/native', () => ({
// Wrapper with required providers
const SignUpScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('SignUpScreen', () => {
diff --git a/mobile/src/screens/__tests__/StakingScreen.test.tsx b/mobile/src/screens/__tests__/StakingScreen.test.tsx
index 09567f7c..b7728ba8 100644
--- a/mobile/src/screens/__tests__/StakingScreen.test.tsx
+++ b/mobile/src/screens/__tests__/StakingScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import StakingScreen from '../StakingScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const StakingScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('StakingScreen', () => {
diff --git a/mobile/src/screens/__tests__/SwapScreen.test.tsx b/mobile/src/screens/__tests__/SwapScreen.test.tsx
index 9692952a..94c64055 100644
--- a/mobile/src/screens/__tests__/SwapScreen.test.tsx
+++ b/mobile/src/screens/__tests__/SwapScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import SwapScreen from '../SwapScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const SwapScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('SwapScreen', () => {
diff --git a/mobile/src/screens/__tests__/WalletScreen.test.tsx b/mobile/src/screens/__tests__/WalletScreen.test.tsx
index f228830b..e1ca1f3a 100644
--- a/mobile/src/screens/__tests__/WalletScreen.test.tsx
+++ b/mobile/src/screens/__tests__/WalletScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import WalletScreen from '../WalletScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const WalletScreenWrapper = () => (
-
-
-
-
-
+
+
+
);
describe('WalletScreen', () => {
diff --git a/mobile/src/screens/__tests__/WelcomeScreen.test.tsx b/mobile/src/screens/__tests__/WelcomeScreen.test.tsx
index 8dbec48d..3177d91f 100644
--- a/mobile/src/screens/__tests__/WelcomeScreen.test.tsx
+++ b/mobile/src/screens/__tests__/WelcomeScreen.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import { LanguageProvider } from '../../contexts/LanguageContext';
import WelcomeScreen from '../WelcomeScreen';
// Mock navigation
@@ -13,9 +12,7 @@ jest.mock('@react-navigation/native', () => ({
// Wrapper with required providers
const WelcomeScreenWrapper = () => (
-
-
-
+
);
describe('WelcomeScreen', () => {
diff --git a/shared/images/HEZ_Token_Logo.svg b/shared/images/HEZ_Token_Logo.svg
new file mode 100644
index 00000000..df3d02ba
--- /dev/null
+++ b/shared/images/HEZ_Token_Logo.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/shared/images/PEZ_Token_Logo.svg b/shared/images/PEZ_Token_Logo.svg
new file mode 100644
index 00000000..0039a3c3
--- /dev/null
+++ b/shared/images/PEZ_Token_Logo.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/shared/images/pezkuwi-wallet.jpg b/shared/images/pezkuwi-wallet.jpg
index ff6f719f..be8b3d20 100644
Binary files a/shared/images/pezkuwi-wallet.jpg and b/shared/images/pezkuwi-wallet.jpg differ
diff --git a/shared/images/pngtosvg/ADAlogo.png b/shared/images/pngtosvg/ADAlogo.png
new file mode 100644
index 00000000..b6374207
Binary files /dev/null and b/shared/images/pngtosvg/ADAlogo.png differ
diff --git a/shared/images/pngtosvg/ADAlogo.svg b/shared/images/pngtosvg/ADAlogo.svg
new file mode 100644
index 00000000..d0a3aaac
--- /dev/null
+++ b/shared/images/pngtosvg/ADAlogo.svg
@@ -0,0 +1,62 @@
+
+
+
diff --git a/shared/images/pngtosvg/B2BplatformLogo.png b/shared/images/pngtosvg/B2BplatformLogo.png
new file mode 100644
index 00000000..407d1fc9
Binary files /dev/null and b/shared/images/pngtosvg/B2BplatformLogo.png differ
diff --git a/shared/images/pngtosvg/B2BplatformLogo.svg b/shared/images/pngtosvg/B2BplatformLogo.svg
new file mode 100644
index 00000000..a88f31b7
--- /dev/null
+++ b/shared/images/pngtosvg/B2BplatformLogo.svg
@@ -0,0 +1,235 @@
+
+
+
diff --git a/shared/images/pngtosvg/BNB_logo.png b/shared/images/pngtosvg/BNB_logo.png
new file mode 100644
index 00000000..d10ef0a8
Binary files /dev/null and b/shared/images/pngtosvg/BNB_logo.png differ
diff --git a/shared/images/pngtosvg/BNB_logo.svg b/shared/images/pngtosvg/BNB_logo.svg
new file mode 100644
index 00000000..b2d5e47b
--- /dev/null
+++ b/shared/images/pngtosvg/BNB_logo.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/shared/images/pngtosvg/BankLogo.png b/shared/images/pngtosvg/BankLogo.png
new file mode 100644
index 00000000..049bb8db
Binary files /dev/null and b/shared/images/pngtosvg/BankLogo.png differ
diff --git a/shared/images/pngtosvg/BankLogo.svg b/shared/images/pngtosvg/BankLogo.svg
new file mode 100644
index 00000000..b2c8d0e1
--- /dev/null
+++ b/shared/images/pngtosvg/BankLogo.svg
@@ -0,0 +1,128 @@
+
+
+
diff --git a/shared/images/pngtosvg/HEZ_Token_Logo.png b/shared/images/pngtosvg/HEZ_Token_Logo.png
new file mode 100644
index 00000000..7ccc022d
Binary files /dev/null and b/shared/images/pngtosvg/HEZ_Token_Logo.png differ
diff --git a/shared/images/pngtosvg/HEZ_Token_Logo.svg b/shared/images/pngtosvg/HEZ_Token_Logo.svg
new file mode 100644
index 00000000..98ec6043
--- /dev/null
+++ b/shared/images/pngtosvg/HEZ_Token_Logo.svg
@@ -0,0 +1,273 @@
+
+
+
diff --git a/shared/images/pngtosvg/PezkuwiChain_Logo_Horizontal_Green_White.png b/shared/images/pngtosvg/PezkuwiChain_Logo_Horizontal_Green_White.png
new file mode 100644
index 00000000..e8c44b85
Binary files /dev/null and b/shared/images/pngtosvg/PezkuwiChain_Logo_Horizontal_Green_White.png differ
diff --git a/shared/images/pngtosvg/PezkuwiChain_Logo_Horizontal_Green_White.svg b/shared/images/pngtosvg/PezkuwiChain_Logo_Horizontal_Green_White.svg
new file mode 100644
index 00000000..066f9d6a
--- /dev/null
+++ b/shared/images/pngtosvg/PezkuwiChain_Logo_Horizontal_Green_White.svg
@@ -0,0 +1,154 @@
+
+
+
diff --git a/shared/images/pngtosvg/Pezkuwi_Logo_Horizontal_Pink_Black.png b/shared/images/pngtosvg/Pezkuwi_Logo_Horizontal_Pink_Black.png
new file mode 100644
index 00000000..a675ec60
Binary files /dev/null and b/shared/images/pngtosvg/Pezkuwi_Logo_Horizontal_Pink_Black.png differ
diff --git a/shared/images/pngtosvg/Pezkuwi_Logo_Horizontal_Pink_Black.svg b/shared/images/pngtosvg/Pezkuwi_Logo_Horizontal_Pink_Black.svg
new file mode 100644
index 00000000..ff777078
--- /dev/null
+++ b/shared/images/pngtosvg/Pezkuwi_Logo_Horizontal_Pink_Black.svg
@@ -0,0 +1,229 @@
+
+
+
diff --git a/shared/images/pngtosvg/USDT(hez)logo.png b/shared/images/pngtosvg/USDT(hez)logo.png
new file mode 100644
index 00000000..30386dc6
Binary files /dev/null and b/shared/images/pngtosvg/USDT(hez)logo.png differ
diff --git a/shared/images/pngtosvg/USDT(hez)logo.svg b/shared/images/pngtosvg/USDT(hez)logo.svg
new file mode 100644
index 00000000..72b03632
--- /dev/null
+++ b/shared/images/pngtosvg/USDT(hez)logo.svg
@@ -0,0 +1,157 @@
+
+
+
diff --git a/shared/images/pngtosvg/adaptive-icon.png b/shared/images/pngtosvg/adaptive-icon.png
new file mode 100644
index 00000000..fa697152
Binary files /dev/null and b/shared/images/pngtosvg/adaptive-icon.png differ
diff --git a/shared/images/pngtosvg/adaptive-icon.svg b/shared/images/pngtosvg/adaptive-icon.svg
new file mode 100644
index 00000000..24a79173
--- /dev/null
+++ b/shared/images/pngtosvg/adaptive-icon.svg
@@ -0,0 +1,29 @@
+
+
+
diff --git a/shared/images/pngtosvg/bitcoin.png b/shared/images/pngtosvg/bitcoin.png
new file mode 100644
index 00000000..51cd6040
Binary files /dev/null and b/shared/images/pngtosvg/bitcoin.png differ
diff --git a/shared/images/pngtosvg/bitcoin.svg b/shared/images/pngtosvg/bitcoin.svg
new file mode 100644
index 00000000..54c099bf
--- /dev/null
+++ b/shared/images/pngtosvg/bitcoin.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/shared/images/pngtosvg/etherium.png b/shared/images/pngtosvg/etherium.png
new file mode 100644
index 00000000..cc0a7d39
Binary files /dev/null and b/shared/images/pngtosvg/etherium.png differ
diff --git a/shared/images/pngtosvg/etherium.svg b/shared/images/pngtosvg/etherium.svg
new file mode 100644
index 00000000..f6f2a70c
--- /dev/null
+++ b/shared/images/pngtosvg/etherium.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/shared/images/pngtosvg/favicon.png b/shared/images/pngtosvg/favicon.png
new file mode 100644
index 00000000..445f8f22
Binary files /dev/null and b/shared/images/pngtosvg/favicon.png differ
diff --git a/shared/images/pngtosvg/favicon.svg b/shared/images/pngtosvg/favicon.svg
new file mode 100644
index 00000000..ecfbc4d5
--- /dev/null
+++ b/shared/images/pngtosvg/favicon.svg
@@ -0,0 +1,38 @@
+
+
+
diff --git a/shared/images/pngtosvg/kurdish-president.png b/shared/images/pngtosvg/kurdish-president.png
new file mode 100644
index 00000000..78d6edf4
Binary files /dev/null and b/shared/images/pngtosvg/kurdish-president.png differ
diff --git a/shared/images/pngtosvg/kurdish-president.svg b/shared/images/pngtosvg/kurdish-president.svg
new file mode 100644
index 00000000..06e73a2e
--- /dev/null
+++ b/shared/images/pngtosvg/kurdish-president.svg
@@ -0,0 +1,528 @@
+
+
+
diff --git a/shared/images/pngtosvg/kurdistan-map.png b/shared/images/pngtosvg/kurdistan-map.png
new file mode 100644
index 00000000..281a3b00
Binary files /dev/null and b/shared/images/pngtosvg/kurdistan-map.png differ
diff --git a/shared/images/pngtosvg/kurdistan-map.svg b/shared/images/pngtosvg/kurdistan-map.svg
new file mode 100644
index 00000000..4f3bea82
--- /dev/null
+++ b/shared/images/pngtosvg/kurdistan-map.svg
@@ -0,0 +1,182 @@
+
+
+
diff --git a/shared/images/pngtosvg/qa_b2b.png b/shared/images/pngtosvg/qa_b2b.png
new file mode 100644
index 00000000..e3ca7ac9
Binary files /dev/null and b/shared/images/pngtosvg/qa_b2b.png differ
diff --git a/shared/images/pngtosvg/qa_b2b.svg b/shared/images/pngtosvg/qa_b2b.svg
new file mode 100644
index 00000000..51b485e7
--- /dev/null
+++ b/shared/images/pngtosvg/qa_b2b.svg
@@ -0,0 +1,963 @@
+
+
+
diff --git a/shared/images/pngtosvg/qa_bank.png b/shared/images/pngtosvg/qa_bank.png
new file mode 100644
index 00000000..9c6e049d
Binary files /dev/null and b/shared/images/pngtosvg/qa_bank.png differ
diff --git a/shared/images/pngtosvg/qa_bank.svg b/shared/images/pngtosvg/qa_bank.svg
new file mode 100644
index 00000000..6940b9b1
--- /dev/null
+++ b/shared/images/pngtosvg/qa_bank.svg
@@ -0,0 +1,1417 @@
+
+
+
diff --git a/shared/images/pngtosvg/qa_education.png b/shared/images/pngtosvg/qa_education.png
new file mode 100644
index 00000000..b2f70638
Binary files /dev/null and b/shared/images/pngtosvg/qa_education.png differ
diff --git a/shared/images/pngtosvg/qa_education.svg b/shared/images/pngtosvg/qa_education.svg
new file mode 100644
index 00000000..6b4ca958
--- /dev/null
+++ b/shared/images/pngtosvg/qa_education.svg
@@ -0,0 +1,631 @@
+
+
+
diff --git a/shared/images/pngtosvg/qa_exchange.png b/shared/images/pngtosvg/qa_exchange.png
new file mode 100644
index 00000000..d69ac483
Binary files /dev/null and b/shared/images/pngtosvg/qa_exchange.png differ
diff --git a/shared/images/pngtosvg/qa_exchange.svg b/shared/images/pngtosvg/qa_exchange.svg
new file mode 100644
index 00000000..ad81b75b
--- /dev/null
+++ b/shared/images/pngtosvg/qa_exchange.svg
@@ -0,0 +1,946 @@
+
+
+
diff --git a/shared/images/pngtosvg/qa_university.png b/shared/images/pngtosvg/qa_university.png
new file mode 100644
index 00000000..adb3ffb8
Binary files /dev/null and b/shared/images/pngtosvg/qa_university.png differ
diff --git a/shared/images/pngtosvg/qa_university.svg b/shared/images/pngtosvg/qa_university.svg
new file mode 100644
index 00000000..36338979
--- /dev/null
+++ b/shared/images/pngtosvg/qa_university.svg
@@ -0,0 +1,259 @@
+
+
+
diff --git a/shared/images/pngtosvg/react-logo.png b/shared/images/pngtosvg/react-logo.png
new file mode 100644
index 00000000..649aa748
Binary files /dev/null and b/shared/images/pngtosvg/react-logo.png differ
diff --git a/shared/images/pngtosvg/react-logo.svg b/shared/images/pngtosvg/react-logo.svg
new file mode 100644
index 00000000..5e1e0936
--- /dev/null
+++ b/shared/images/pngtosvg/react-logo.svg
@@ -0,0 +1,43 @@
+
+
+
diff --git a/shared/images/pngtosvg/vicdanlogo.jpg b/shared/images/pngtosvg/vicdanlogo.jpg
new file mode 100644
index 00000000..b6e8f9ab
Binary files /dev/null and b/shared/images/pngtosvg/vicdanlogo.jpg differ
diff --git a/shared/images/pngtosvg/vicdanlogo.svg b/shared/images/pngtosvg/vicdanlogo.svg
new file mode 100644
index 00000000..e258272e
--- /dev/null
+++ b/shared/images/pngtosvg/vicdanlogo.svg
@@ -0,0 +1,113 @@
+
+
+