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 */} + + + + + + + + + + {/* Sun rays (21 rays for Kurdistan flag) */} + {rays} + + {/* Central white circle */} + + + {/* Inner glow */} + + + + + ); +}; + +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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + 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 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + +