refactor(mobile): Remove i18n, expand core screens, update plan

BREAKING: Removed multi-language support (i18n) - will be re-added later

Changes:
- Removed i18n system (6 language files, LanguageContext)
- Expanded WalletScreen, SettingsScreen, SwapScreen with more features
- Added KurdistanSun component, HEZ/PEZ token icons
- Added EditProfileScreen, WalletSetupScreen
- Added button e2e tests (Profile, Settings, Wallet)
- Updated plan: honest assessment - 42 nav buttons with mock data
- Fixed terminology: Polkadot→Pezkuwi, Substrate→Bizinikiwi

Reality check: UI complete with mock data, converting to production one-by-one
This commit is contained in:
2026-01-15 05:08:21 +03:00
parent 92e5831f7c
commit 24d6a942f8
110 changed files with 11157 additions and 3260 deletions
@@ -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,
@@ -7,20 +7,36 @@ interface BiometricAuthContextType {
isBiometricAvailable: boolean;
biometricType: 'fingerprint' | 'facial' | 'iris' | 'none';
isBiometricEnabled: boolean;
isLocked: boolean;
autoLockTimer: number;
authenticate: () => Promise<boolean>;
enableBiometric: () => Promise<boolean>;
disableBiometric: () => Promise<void>;
setPinCode: (pin: string) => Promise<void>;
verifyPinCode: (pin: string) => Promise<boolean>;
setAutoLockTimer: (minutes: number) => Promise<void>;
lock: () => void;
unlock: () => void;
checkAutoLock: () => Promise<void>;
}
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<BiometricAuthContextType>(mockBiometricContext);
@@ -19,7 +19,7 @@ interface ThemeContextType {
fontScale: number;
}
const mockThemeContext: ThemeContextType = {
export const mockThemeContext: ThemeContextType = {
isDarkMode: false,
toggleDarkMode: jest.fn().mockResolvedValue(undefined),
colors: LightColors,
@@ -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<typeof mockThemeContext>;
auth?: Partial<typeof mockAuthContext>;
} = {}) => {
return render(
<MockAuthProvider value={overrides.auth}>
<MockThemeProvider value={overrides.theme}>
<ProfileScreen />
</MockThemeProvider>
</MockAuthProvider>
);
};
const renderEditProfileScreen = (overrides: {
theme?: Partial<typeof mockThemeContext>;
auth?: Partial<typeof mockAuthContext>;
} = {}) => {
return render(
<MockAuthProvider value={overrides.auth}>
<MockThemeProvider value={overrides.theme}>
<EditProfileScreen />
</MockThemeProvider>
</MockAuthProvider>
);
};
// ============================================================
// 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
});
});
});
});
@@ -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<typeof mockThemeContext>;
biometric?: Partial<typeof mockBiometricContext>;
auth?: Partial<typeof mockAuthContext>;
} = {}) => {
return render(
<MockAuthProvider value={overrides.auth}>
<MockBiometricAuthProvider value={overrides.biometric}>
<MockThemeProvider value={overrides.theme}>
<SettingsScreen />
</MockThemeProvider>
</MockBiometricAuthProvider>
</MockAuthProvider>
);
};
// ============================================================
// 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();
});
});
});
@@ -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(
<MockAuthProvider>
<MockThemeProvider>
<WalletSetupScreen />
</MockThemeProvider>
</MockAuthProvider>
);
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();
});
});
});
@@ -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(
<MockAuthProvider value={authValue}>
<MockBiometricAuthProvider value={biometricValue}>
<MockThemeProvider value={themeValue}>
<SettingsScreen />
</MockThemeProvider>
</MockBiometricAuthProvider>
</MockAuthProvider>
);
};
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);
});
});
});
});
@@ -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(
<MockAuthProvider value={authValue}>
<MockBiometricAuthProvider value={biometricValue}>
<MockThemeProvider value={themeValue}>
<SettingsScreen />
</MockThemeProvider>
</MockBiometricAuthProvider>
</MockAuthProvider>
);
};
// 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(
<MockThemeProvider value={themeValue}>
<FontSizeModal {...props} />
</MockThemeProvider>
),
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();
});
});
});
});
+3 -6
View File
@@ -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 (
<MockAuthProvider>
<MockPezkuwiProvider>
<MockLanguageProvider>
<MockBiometricAuthProvider>
{children}
</MockBiometricAuthProvider>
</MockLanguageProvider>
<MockBiometricAuthProvider>
{children}
</MockBiometricAuthProvider>
</MockPezkuwiProvider>
</MockAuthProvider>
);
+67 -32
View File
@@ -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<AvatarPickerModalProps> = ({
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<AvatarPickerModalProps> = ({
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<string | null> => {
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<AvatarPickerModalProps> = ({
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<AvatarPickerModalProps> = ({
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<AvatarPickerModalProps> = ({
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);
}
+224
View File
@@ -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<KurdistanSunProps> = ({ 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 (
<Line
key={i}
x1="100"
y1="100"
x2="100"
y2="20"
stroke="rgba(255, 255, 255, 0.9)"
strokeWidth="3"
strokeLinecap="round"
transform={`rotate(${angle} 100 100)`}
/>
);
});
return (
<View style={[styles.container, { width: size, height: size }]}>
{/* Rotating colored halos */}
<View style={styles.halosContainer}>
{/* Green halo (outermost) */}
<AnimatedView
style={[
styles.halo,
{
width: haloSize,
height: haloSize,
borderWidth: borderWidth,
borderTopColor: '#00FF00',
borderBottomColor: '#00FF00',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
transform: [{ rotate: greenSpin }],
},
]}
/>
{/* Red halo (middle) */}
<AnimatedView
style={[
styles.halo,
{
width: haloSize * 0.8,
height: haloSize * 0.8,
borderWidth: borderWidth,
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
borderLeftColor: '#FF0000',
borderRightColor: '#FF0000',
transform: [{ rotate: redSpin }],
},
]}
/>
{/* Yellow halo (inner) */}
<AnimatedView
style={[
styles.halo,
{
width: haloSize * 0.6,
height: haloSize * 0.6,
borderWidth: borderWidth,
borderTopColor: '#FFD700',
borderBottomColor: '#FFD700',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
transform: [{ rotate: yellowSpin }],
},
]}
/>
</View>
{/* Kurdistan Sun SVG with 21 rays */}
<AnimatedView style={[styles.svgContainer, { opacity: raysPulse }]}>
<Svg width={size} height={size} viewBox="0 0 200 200">
<Defs>
<RadialGradient id="sunGradient" cx="50%" cy="50%" r="50%">
<Stop offset="0%" stopColor="rgba(255, 255, 255, 0.8)" />
<Stop offset="100%" stopColor="rgba(255, 255, 255, 0.2)" />
</RadialGradient>
</Defs>
{/* Sun rays (21 rays for Kurdistan flag) */}
{rays}
{/* Central white circle */}
<Circle cx="100" cy="100" r="35" fill="white" />
{/* Inner glow */}
<Circle cx="100" cy="100" r="35" fill="url(#sunGradient)" />
</Svg>
</AnimatedView>
</View>
);
};
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;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
export { default as HezTokenLogo } from './HezTokenLogo';
export { default as PezTokenLogo } from './PezTokenLogo';
-64
View File
@@ -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<LanguageContextType | undefined>(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 (
<LanguageContext.Provider
value={{
currentLanguage,
isRTL: currentIsRTL,
hasSelectedLanguage: true, // Always true - language pre-selected at build time
availableLanguages: languages,
}}
>
{children}
</LanguageContext.Provider>
);
};
export const useLanguage = (): LanguageContextType => {
const context = useContext(LanguageContext);
if (!context) {
throw new Error('useLanguage must be used within LanguageProvider');
}
return context;
};
+114 -23
View File
@@ -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<void> => {
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<string | null> => {
if (Platform.OS === 'web') {
return await AsyncStorage.getItem(key);
} else {
return await SecureStore.getItemAsync(key);
}
},
removeItem: async (key: string): Promise<void> => {
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<NetworkType, NetworkConfig> = {
@@ -54,6 +83,13 @@ export const NETWORKS: Record<NetworkType, NetworkConfig> = {
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<void>;
getKeyPair: (address: string) => Promise<KeyringPair | null>;
signMessage: (address: string, message: string) => Promise<string | null>;
error: string | null;
@@ -131,7 +168,14 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({ 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<PezkuwiProviderProps> = ({ 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<PezkuwiProviderProps> = ({ 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<PezkuwiProviderProps> = ({ 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<void> => {
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<PezkuwiProviderProps> = ({ 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<PezkuwiProviderProps> = ({ children }) =>
disconnectWallet,
createWallet,
importWallet,
deleteWallet,
getKeyPair,
signMessage,
error,
@@ -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 }) => (
<LanguageProvider>{children}</LanguageProvider>
);
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');
});
});
-22
View File
@@ -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');
});
});
-55
View File
@@ -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;
-228
View File
@@ -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": "اختياري"
}
}
-228
View File
@@ -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": "ئیختیاری"
}
}
-228
View File
@@ -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"
}
}
-163
View File
@@ -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": "اختیاری"
}
}
-228
View File
@@ -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"
}
}
-228
View File
@@ -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ı"
}
}
+36
View File
@@ -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',
}}
/>
<Stack.Screen
name="EditProfile"
component={EditProfileScreen}
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="Wallet"
component={WalletScreen}
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="WalletSetup"
component={WalletSetupScreen}
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="Swap"
component={SwapScreen}
options={{
headerShown: false,
}}
/>
</>
)}
</Stack.Navigator>
+30 -32
View File
@@ -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 = () => {
</View>
<Text style={styles.brandTitle}>PezkuwiChain</Text>
<Text style={styles.subtitle}>
{t('login.subtitle', 'Access your governance account')}
Access your governance account
</Text>
</View>
@@ -158,7 +156,7 @@ const AuthScreen: React.FC = () => {
}}
>
<Text style={[styles.tabText, activeTab === 'signin' && styles.tabTextActive]}>
{t('login.signin', 'Sign In')}
Sign In
</Text>
</TouchableOpacity>
<TouchableOpacity
@@ -169,7 +167,7 @@ const AuthScreen: React.FC = () => {
}}
>
<Text style={[styles.tabText, activeTab === 'signup' && styles.tabTextActive]}>
{t('login.signup', 'Sign Up')}
Sign Up
</Text>
</TouchableOpacity>
</View>
@@ -178,7 +176,7 @@ const AuthScreen: React.FC = () => {
{activeTab === 'signin' && (
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.email', 'Email')}</Text>
<Text style={styles.label}>Email</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}></Text>
<TextInput
@@ -195,7 +193,7 @@ const AuthScreen: React.FC = () => {
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.password', 'Password')}</Text>
<Text style={styles.label}>Password</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>🔒</Text>
<TextInput
@@ -225,12 +223,12 @@ const AuthScreen: React.FC = () => {
{rememberMe && <Text style={styles.checkmark}></Text>}
</View>
<Text style={styles.checkboxLabel}>
{t('login.rememberMe', 'Remember me')}
Remember me
</Text>
</TouchableOpacity>
<TouchableOpacity>
<Text style={styles.linkText}>
{t('login.forgotPassword', 'Forgot password?')}
Forgot password?
</Text>
</TouchableOpacity>
</View>
@@ -251,7 +249,7 @@ const AuthScreen: React.FC = () => {
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.primaryButtonText}>
{t('login.signin', 'Sign In')}
Sign In
</Text>
)}
</TouchableOpacity>
@@ -262,7 +260,7 @@ const AuthScreen: React.FC = () => {
{activeTab === 'signup' && (
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.fullName', 'Full Name')}</Text>
<Text style={styles.label}>Full Name</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>👤</Text>
<TextInput
@@ -277,7 +275,7 @@ const AuthScreen: React.FC = () => {
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.email', 'Email')}</Text>
<Text style={styles.label}>Email</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}></Text>
<TextInput
@@ -294,7 +292,7 @@ const AuthScreen: React.FC = () => {
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.password', 'Password')}</Text>
<Text style={styles.label}>Password</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>🔒</Text>
<TextInput
@@ -316,7 +314,7 @@ const AuthScreen: React.FC = () => {
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.confirmPassword', 'Confirm Password')}</Text>
<Text style={styles.label}>Confirm Password</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>🔒</Text>
<TextInput
@@ -333,16 +331,16 @@ const AuthScreen: React.FC = () => {
<View style={styles.inputGroup}>
<Text style={styles.label}>
{t('login.referralCode', 'Referral Code')}{' '}
Referral Code{' '}
<Text style={styles.optionalText}>
({t('login.optional', 'Optional')})
(Optional)
</Text>
</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>👥</Text>
<TextInput
style={styles.input}
placeholder={t('login.enterReferralCode', 'Referral code (optional)')}
placeholder="Referral code (optional)"
placeholderTextColor="#9CA3AF"
value={signupReferralCode}
onChangeText={setSignupReferralCode}
@@ -350,7 +348,7 @@ const AuthScreen: React.FC = () => {
/>
</View>
<Text style={styles.hintText}>
{t('login.referralDescription', 'If someone referred you, enter their code here')}
If someone referred you, enter their code here
</Text>
</View>
@@ -370,7 +368,7 @@ const AuthScreen: React.FC = () => {
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.primaryButtonText}>
{t('login.createAccount', 'Create Account')}
Create Account
</Text>
)}
</TouchableOpacity>
@@ -380,15 +378,15 @@ const AuthScreen: React.FC = () => {
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.footerText}>
{t('login.terms', 'By continuing, you agree to our')}{' '}
By continuing, you agree to our{' '}
</Text>
<View style={styles.footerLinks}>
<Text style={styles.footerLink}>
{t('login.termsOfService', 'Terms of Service')}
Terms of Service
</Text>
<Text style={styles.footerText}> {t('login.and', 'and')} </Text>
<Text style={styles.footerText}> and </Text>
<Text style={styles.footerLink}>
{t('login.privacyPolicy', 'Privacy Policy')}
Privacy Policy
</Text>
</View>
</View>
@@ -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);
@@ -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<NavigationProp<RootStackParamList>>();
return (
@@ -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);
-2
View File
@@ -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');
+4 -19
View File
@@ -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<DashboardScreenProps> = () => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<BottomTabParamList & RootStackParamList>>();
const { user } = useAuth();
const { api, isApiReady, selectedAccount } = usePezkuwi();
@@ -171,8 +169,8 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
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<DashboardScreenProps> = () => {
</TouchableOpacity>
</View>
<View style={styles.appsGrid}>
{/* 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)}
+450
View File
@@ -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<string | null>(null);
const [originalName, setOriginalName] = useState('');
const [originalAvatar, setOriginalAvatar] = useState<string | null>(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 (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="edit-profile-loading">
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<Text style={[styles.loadingText, { color: colors.textSecondary, fontSize: 14 * fontScale }]}>
Loading profile...
</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="edit-profile-screen">
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoid}
>
{/* Header */}
<View style={[styles.header, { borderBottomColor: colors.border }]} testID="edit-profile-header">
<TouchableOpacity onPress={handleCancel} testID="edit-profile-cancel-button">
<Text style={[styles.headerButton, { color: colors.textSecondary, fontSize: 16 * fontScale }]}>
Cancel
</Text>
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: colors.text, fontSize: 18 * fontScale }]}>
Edit Profile
</Text>
<TouchableOpacity
onPress={handleSave}
disabled={saving || !hasChanges()}
testID="edit-profile-save-button"
>
{saving ? (
<ActivityIndicator size="small" color={KurdistanColors.kesk} />
) : (
<Text style={[
styles.headerButton,
styles.saveButton,
{ fontSize: 16 * fontScale },
!hasChanges() && styles.saveButtonDisabled
]}>
Save
</Text>
)}
</TouchableOpacity>
</View>
<ScrollView
style={styles.content}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
testID="edit-profile-scroll"
>
{/* Avatar Section */}
<View style={styles.avatarSection} testID="edit-profile-avatar-section">
<TouchableOpacity
onPress={() => setAvatarModalVisible(true)}
style={styles.avatarButton}
testID="edit-profile-avatar-button"
>
<View style={[styles.avatarCircle, { backgroundColor: colors.surface }]}>
{avatarUrl ? (
<Text style={styles.avatarEmoji}>{getEmojiFromAvatarId(avatarUrl)}</Text>
) : (
<Text style={[styles.avatarInitial, { color: colors.textSecondary }]}>
{fullName?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || '?'}
</Text>
)}
</View>
<View style={styles.editAvatarBadge}>
<Text style={styles.editAvatarIcon}>📷</Text>
</View>
</TouchableOpacity>
<Text style={[styles.changePhotoText, { color: KurdistanColors.kesk, fontSize: 14 * fontScale }]}>
Change Avatar
</Text>
</View>
{/* Form Section */}
<View style={styles.formSection}>
{/* Display Name */}
<View style={styles.inputGroup} testID="edit-profile-name-group">
<Text style={[styles.inputLabel, { color: colors.textSecondary, fontSize: 14 * fontScale }]}>
Display Name
</Text>
<TextInput
style={[styles.textInput, {
backgroundColor: colors.surface,
color: colors.text,
borderColor: colors.border,
fontSize: 16 * fontScale
}]}
value={fullName}
onChangeText={setFullName}
placeholder="Enter your display name"
placeholderTextColor={colors.textSecondary}
autoCapitalize="words"
autoCorrect={false}
testID="edit-profile-name-input"
/>
<Text style={[styles.inputHint, { color: colors.textSecondary, fontSize: 12 * fontScale }]}>
This is how other users will see you
</Text>
</View>
{/* Email (Read-only) */}
<View style={styles.inputGroup} testID="edit-profile-email-group">
<Text style={[styles.inputLabel, { color: colors.textSecondary, fontSize: 14 * fontScale }]}>
Email
</Text>
<View style={[styles.readOnlyField, { backgroundColor: colors.background, borderColor: colors.border }]}>
<Text style={[styles.readOnlyText, { color: colors.textSecondary, fontSize: 16 * fontScale }]}>
{user?.email || 'N/A'}
</Text>
<Text style={styles.lockIcon}>🔒</Text>
</View>
<Text style={[styles.inputHint, { color: colors.textSecondary, fontSize: 12 * fontScale }]}>
Email cannot be changed
</Text>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
{/* Avatar Picker Modal */}
<AvatarPickerModal
visible={avatarModalVisible}
onClose={() => setAvatarModalVisible(false)}
currentAvatar={avatarUrl || undefined}
onAvatarSelected={handleAvatarSelected}
/>
</SafeAreaView>
);
};
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;
+77 -37
View File
@@ -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<any>();
const { user, signOut } = useAuth();
const { isDarkMode, colors, fontScale } = useTheme();
const [profileData, setProfileData] = useState<ProfileData | null>(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 }) => (
<TouchableOpacity style={styles.profileCard} onPress={onPress} disabled={!onPress} activeOpacity={onPress ? 0.7 : 1}>
const handleEditProfile = () => {
navigation.navigate('EditProfile');
};
const ProfileCard = ({ icon, title, value, onPress, testID }: { icon: string; title: string; value: string; onPress?: () => void; testID?: string }) => (
<TouchableOpacity
style={[styles.profileCard, { backgroundColor: colors.surface }]}
onPress={onPress}
disabled={!onPress}
activeOpacity={onPress ? 0.7 : 1}
testID={testID}
>
<Text style={styles.cardIcon}>{icon}</Text>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{title}</Text>
<Text style={styles.cardValue} numberOfLines={1}>{value}</Text>
<Text style={[styles.cardTitle, { fontSize: 12 * fontScale }]}>{title}</Text>
<Text style={[styles.cardValue, { color: colors.text, fontSize: 16 * fontScale }]} numberOfLines={1}>{value}</Text>
</View>
{onPress && <Text style={styles.cardArrow}></Text>}
</TouchableOpacity>
@@ -133,41 +164,42 @@ const ProfileScreen: React.FC = () => {
if (loading) {
return (
<SafeAreaView style={styles.container}>
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="profile-loading-container">
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<ActivityIndicator size="large" color={KurdistanColors.kesk} testID="profile-loading-indicator" />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="profile-screen">
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<ScrollView showsVerticalScrollIndicator={false}>
<ScrollView showsVerticalScrollIndicator={false} testID="profile-scroll-view">
{/* Header with Gradient */}
<LinearGradient
colors={[KurdistanColors.kesk, '#008f43']}
style={styles.header}
testID="profile-header-gradient"
>
<View style={styles.avatarContainer}>
<TouchableOpacity onPress={() => setAvatarModalVisible(true)} style={styles.avatarWrapper}>
<TouchableOpacity onPress={() => 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') ? (
<Image source={{ uri: profileData.avatar_url }} style={styles.avatar} />
<Image source={{ uri: profileData.avatar_url }} style={styles.avatar} testID="profile-avatar-image" />
) : (
// It's an emoji ID, render as emoji text
<View style={styles.avatarPlaceholder}>
<Text style={styles.avatarEmojiLarge}>
<View style={styles.avatarPlaceholder} testID="profile-avatar-emoji-container">
<Text style={styles.avatarEmojiLarge} testID="profile-avatar-emoji">
{getEmojiFromAvatarId(profileData.avatar_url)}
</Text>
</View>
)
) : (
<View style={styles.avatarPlaceholder}>
<Text style={styles.avatarText}>
<View style={styles.avatarPlaceholder} testID="profile-avatar-placeholder">
<Text style={styles.avatarText} testID="profile-avatar-initial">
{profileData?.full_name?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || '?'}
</Text>
</View>
@@ -176,25 +208,27 @@ const ProfileScreen: React.FC = () => {
<Text style={styles.editAvatarIcon}>📷</Text>
</View>
</TouchableOpacity>
<Text style={styles.name}>
<Text style={[styles.name, { fontSize: 24 * fontScale }]} testID="profile-name">
{profileData?.full_name || user?.email?.split('@')[0] || 'User'}
</Text>
<Text style={styles.email}>{user?.email}</Text>
<Text style={[styles.email, { fontSize: 14 * fontScale }]} testID="profile-email">{user?.email}</Text>
</View>
</LinearGradient>
{/* Profile Info Cards */}
<View style={styles.cardsContainer}>
<View style={styles.cardsContainer} testID="profile-cards-container">
<ProfileCard
icon="📧"
title="Email"
value={user?.email || 'N/A'}
testID="profile-card-email"
/>
<ProfileCard
icon="📅"
title="Member Since"
value={profileData?.created_at ? new Date(profileData.created_at).toLocaleDateString() : 'N/A'}
testID="profile-card-member-since"
/>
<ProfileCard
@@ -202,6 +236,7 @@ const ProfileScreen: React.FC = () => {
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"
/>
)}
</View>
{/* Action Buttons */}
<View style={styles.actionsContainer}>
<View style={styles.actionsContainer} testID="profile-actions-container">
<TouchableOpacity
style={styles.actionButton}
onPress={() => Alert.alert('Coming Soon', 'Edit profile feature will be available soon')}
style={[styles.actionButton, { backgroundColor: colors.surface }]}
onPress={handleEditProfile}
testID="profile-edit-button"
>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionText}>Edit Profile</Text>
<Text style={[styles.actionText, { color: colors.text, fontSize: 16 * fontScale }]}>Edit Profile</Text>
<Text style={styles.actionArrow}></Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => 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"
>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionText}>About Pezkuwi</Text>
<Text style={[styles.actionText, { color: colors.text, fontSize: 16 * fontScale }]}>About Pezkuwi</Text>
<Text style={styles.actionArrow}></Text>
</TouchableOpacity>
</View>
@@ -251,15 +290,16 @@ const ProfileScreen: React.FC = () => {
style={styles.logoutButton}
onPress={handleLogout}
activeOpacity={0.8}
testID="profile-logout-button"
>
<Text style={styles.logoutButtonText}>Logout</Text>
<Text style={[styles.logoutButtonText, { fontSize: 16 * fontScale }]}>Logout</Text>
</TouchableOpacity>
<View style={styles.footer}>
<Text style={styles.footerText}>
<View style={styles.footer} testID="profile-footer">
<Text style={[styles.footerText, { color: colors.textSecondary, fontSize: 12 * fontScale }]}>
Pezkuwi Blockchain {new Date().getFullYear()}
</Text>
<Text style={styles.footerVersion}>Version 1.0.0</Text>
<Text style={[styles.footerVersion, { fontSize: 10 * fontScale }]}>Version 1.0.0</Text>
</View>
</ScrollView>
-2
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+10 -12
View File
@@ -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<SignInScreenProps> = ({ onSignIn, onNavigateToSignUp }) => {
const { t } = useTranslation();
const { signIn } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -78,17 +76,17 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
<View style={styles.logoContainer}>
<Text style={styles.logoText}>PZK</Text>
</View>
<Text style={styles.title}>{t('auth.welcomeBack')}</Text>
<Text style={styles.subtitle}>{t('auth.signIn')}</Text>
<Text style={styles.title}>Welcome Back!</Text>
<Text style={styles.subtitle}>Sign In</Text>
</View>
{/* Form */}
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.email')}</Text>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
placeholder={t('auth.email')}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
@@ -98,10 +96,10 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.password')}</Text>
<Text style={styles.label}>Password</Text>
<TextInput
style={styles.input}
placeholder={t('auth.password')}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
@@ -111,7 +109,7 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
<TouchableOpacity style={styles.forgotPassword}>
<Text style={styles.forgotPasswordText}>
{t('auth.forgotPassword')}
Forgot Password?
</Text>
</TouchableOpacity>
@@ -124,7 +122,7 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
{isLoading ? (
<ActivityIndicator color={KurdistanColors.spi} />
) : (
<Text style={styles.signInButtonText}>{t('auth.signIn')}</Text>
<Text style={styles.signInButtonText}>Sign In</Text>
)}
</TouchableOpacity>
@@ -139,8 +137,8 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
onPress={onNavigateToSignUp}
>
<Text style={styles.signUpPromptText}>
{t('auth.noAccount')}{' '}
<Text style={styles.signUpLink}>{t('auth.signUp')}</Text>
Don't have an account?{' '}
<Text style={styles.signUpLink}>Sign Up</Text>
</Text>
</TouchableOpacity>
</View>
+13 -15
View File
@@ -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<SignUpScreenProps> = ({ onSignUp, onNavigateToSignIn }) => {
const { t } = useTranslation();
const { signUp } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -85,17 +83,17 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
<View style={styles.logoContainer}>
<Text style={styles.logoText}>PZK</Text>
</View>
<Text style={styles.title}>{t('auth.getStarted')}</Text>
<Text style={styles.subtitle}>{t('auth.createAccount')}</Text>
<Text style={styles.title}>Get Started</Text>
<Text style={styles.subtitle}>Create Account</Text>
</View>
{/* Form */}
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.email')}</Text>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
placeholder={t('auth.email')}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
@@ -105,10 +103,10 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.username')}</Text>
<Text style={styles.label}>Username</Text>
<TextInput
style={styles.input}
placeholder={t('auth.username')}
placeholder="Username"
value={username}
onChangeText={setUsername}
autoCapitalize="none"
@@ -117,10 +115,10 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.password')}</Text>
<Text style={styles.label}>Password</Text>
<TextInput
style={styles.input}
placeholder={t('auth.password')}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
@@ -129,10 +127,10 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.confirmPassword')}</Text>
<Text style={styles.label}>Confirm Password</Text>
<TextInput
style={styles.input}
placeholder={t('auth.confirmPassword')}
placeholder="Confirm Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
@@ -149,7 +147,7 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
{isLoading ? (
<ActivityIndicator color={KurdistanColors.spi} />
) : (
<Text style={styles.signUpButtonText}>{t('auth.signUp')}</Text>
<Text style={styles.signUpButtonText}>Sign Up</Text>
)}
</TouchableOpacity>
@@ -164,8 +162,8 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
onPress={onNavigateToSignIn}
>
<Text style={styles.signInPromptText}>
{t('auth.haveAccount')}{' '}
<Text style={styles.signInLink}>{t('auth.signIn')}</Text>
Already have an account?{' '}
<Text style={styles.signInLink}>Sign In</Text>
</Text>
</TouchableOpacity>
</View>
+35 -10
View File
@@ -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);
+233 -29
View File
@@ -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<any>();
const { api, isApiReady, selectedAccount, getKeyPair } = usePezkuwi();
const [fromToken, setFromToken] = useState<TokenInfo>(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<TransactionStatus>('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 (
<SafeAreaView style={styles.container}>
{/* Transaction Loading Overlay */}
{/* Kurdistan Sun Loading Overlay */}
{(txStatus === 'signing' || txStatus === 'submitting') && (
<View style={styles.loadingOverlay}>
<View style={styles.loadingCard}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<Text style={styles.loadingText}>
{txStatus === 'signing' ? 'Waiting for signature...' : 'Processing swap...'}
</Text>
</View>
<KurdistanSun size={250} />
<Text style={styles.loadingText}>
{txStatus === 'signing' ? 'Waiting for signature...' : 'Processing your swap...'}
</Text>
</View>
)}
<ScrollView style={styles.scrollContent} contentContainerStyle={styles.scrollContentContainer}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
<Text style={styles.backButtonText}></Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>Swap Tokens</Text>
<TouchableOpacity onPress={() => setShowSettings(true)} style={styles.settingsButton}>
<Text style={styles.settingsIcon}></Text>
@@ -369,7 +555,13 @@ const SwapScreen: React.FC = () => {
<View style={styles.detailsCard}>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}> Exchange Rate</Text>
<Text style={styles.detailValue}>1 {fromToken.symbol} 1 {toToken.symbol}</Text>
<Text style={styles.detailValue}>
{isLoadingRate
? 'Loading...'
: exchangeRate > 0
? `1 ${fromToken.symbol}${exchangeRate.toFixed(4)} ${toToken.symbol}`
: 'No pool available'}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Slippage Tolerance</Text>
@@ -389,14 +581,16 @@ const SwapScreen: React.FC = () => {
<TouchableOpacity
style={[
styles.swapButton,
(!fromAmount || hasInsufficientBalance || txStatus !== 'idle') && styles.swapButtonDisabled
(!fromAmount || hasInsufficientBalance || txStatus !== 'idle' || exchangeRate === 0) && styles.swapButtonDisabled
]}
onPress={() => setShowConfirm(true)}
disabled={!fromAmount || hasInsufficientBalance || txStatus !== 'idle'}
disabled={!fromAmount || hasInsufficientBalance || txStatus !== 'idle' || exchangeRate === 0}
>
<Text style={styles.swapButtonText}>
{hasInsufficientBalance
? `Insufficient ${fromToken.symbol} Balance`
: exchangeRate === 0
? 'No Pool Available'
: 'Swap Tokens'}
</Text>
</TouchableOpacity>
@@ -468,6 +662,10 @@ const SwapScreen: React.FC = () => {
<Text style={styles.confirmValue}>{toAmount} {toToken.symbol}</Text>
</View>
<View style={[styles.confirmRow, styles.confirmRowBorder]}>
<Text style={styles.confirmLabelSmall}>Exchange Rate</Text>
<Text style={styles.confirmValueSmall}>1 {fromToken.symbol} = {exchangeRate.toFixed(4)} {toToken.symbol}</Text>
</View>
<View style={styles.confirmRow}>
<Text style={styles.confirmLabelSmall}>Slippage</Text>
<Text style={styles.confirmValueSmall}>{slippage}%</Text>
</View>
@@ -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,
+6 -8
View File
@@ -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<VerifyHumanScreenProps> = ({ onVerified }) => {
const { t } = useTranslation();
const [isChecked, setIsChecked] = useState(false);
const [scaleValue] = useState(new Animated.Value(1));
@@ -71,9 +69,9 @@ const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) =>
</View>
{/* Title */}
<Text style={styles.title}>{t('verify.title', 'Security Verification')}</Text>
<Text style={styles.title}>Security Verification</Text>
<Text style={styles.subtitle}>
{t('verify.subtitle', 'Please confirm you are human to continue')}
Please confirm you are human to continue
</Text>
{/* Verification Box */}
@@ -86,13 +84,13 @@ const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) =>
{isChecked && <Text style={styles.checkmark}></Text>}
</View>
<Text style={styles.verificationText}>
{t('verify.checkbox', "I'm not a robot")}
I'm not a robot
</Text>
</TouchableOpacity>
{/* Info Text */}
<Text style={styles.infoText}>
{t('verify.info', 'This helps protect the Pezkuwi network from automated attacks')}
This helps protect the Pezkuwi network from automated attacks
</Text>
{/* Continue Button */}
@@ -109,7 +107,7 @@ const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) =>
!isChecked && styles.continueButtonTextDisabled,
]}
>
{t('verify.continue', 'Continue')}
Continue
</Text>
</Animated.View>
</TouchableOpacity>
@@ -117,7 +115,7 @@ const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) =>
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.footerText}>
🔒 {t('verify.secure', 'Secure & Private')}
Secure & Private
</Text>
</View>
</View>
+425 -159
View File
@@ -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<string | null> => {
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<any>();
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 (
<SafeAreaView style={styles.container}>
<LinearGradient
colors={['#00693E', '#008f43', '#00A651']}
style={styles.welcomeGradient}
>
<View style={styles.welcomeContent}>
<Text style={styles.welcomeEmoji}>🔐</Text>
<Text style={styles.welcomeTitle}>Welcome to</Text>
<Text style={styles.welcomeBrand}>Pezkuwichain Wallet</Text>
<Text style={styles.welcomeSubtitle}>
Secure, Fast & Decentralized
</Text>
</View>
</LinearGradient>
// Redirect to WalletSetupScreen if no wallet exists
useEffect(() => {
if (!selectedAccount && accounts.length === 0) {
navigation.replace('WalletSetup');
}
}, [selectedAccount, accounts, navigation]);
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.primaryWalletButton}
onPress={handleConnectWallet}
activeOpacity={0.8}
>
<LinearGradient
colors={[KurdistanColors.kesk, '#00A651']}
style={styles.buttonGradient}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
>
<View style={styles.buttonIcon}>
<Text style={styles.buttonIconText}></Text>
</View>
<View style={styles.buttonTextContainer}>
<Text style={styles.primaryButtonText}>Create New Wallet</Text>
<Text style={styles.primaryButtonSubtext}>
Get started in seconds
</Text>
</View>
</LinearGradient>
</TouchableOpacity>
<TouchableOpacity
style={styles.secondaryWalletButton}
onPress={() => setImportWalletModalVisible(true)}
activeOpacity={0.8}
>
<View style={styles.secondaryButtonContent}>
<View style={[styles.buttonIcon, {backgroundColor: 'rgba(0,105,62,0.1)'}]}>
<Text style={[styles.buttonIconText, {color: KurdistanColors.kesk}]}>📥</Text>
</View>
<View style={styles.buttonTextContainer}>
<Text style={styles.secondaryButtonText}>Import Existing Wallet</Text>
<Text style={styles.secondaryButtonSubtext}>
Use your seed phrase
</Text>
</View>
</View>
</TouchableOpacity>
<View style={styles.securityNotice}>
<Text style={styles.securityIcon}>🛡</Text>
<Text style={styles.securityText}>
Your keys are encrypted and stored locally on your device
</Text>
</View>
</View>
{/* Create Wallet Modal */}
<Modal visible={createWalletModalVisible} transparent animationType="slide" onRequestClose={() => setCreateWalletModalVisible(false)}>
<View style={styles.modalOverlay}>
<View style={styles.modalCard}>
<Text style={styles.modalHeader}>Create New Wallet</Text>
<TextInput style={styles.inputField} placeholder="Wallet Name" value={walletName} onChangeText={setWalletName} />
<View style={styles.modalActions}>
<TouchableOpacity style={styles.btnCancel} onPress={() => setCreateWalletModalVisible(false)}><Text>Cancel</Text></TouchableOpacity>
<TouchableOpacity style={styles.btnConfirm} onPress={handleCreateWallet}><Text style={{color:'white'}}>Create</Text></TouchableOpacity>
</View>
</View>
</View>
</Modal>
{/* Import Wallet Modal */}
<Modal visible={importWalletModalVisible} transparent animationType="slide" onRequestClose={() => setImportWalletModalVisible(false)}>
<View style={styles.modalOverlay}>
<View style={styles.modalCard}>
<Text style={styles.modalHeader}>Import Wallet</Text>
<Text style={{color: '#666', fontSize: 12, marginBottom: 12}}>Enter your 12 or 24 word mnemonic phrase</Text>
<TextInput
style={[styles.inputField, {height: 100, textAlignVertical: 'top'}]}
placeholder="word1 word2 word3..."
multiline
value={importMnemonic}
onChangeText={setImportMnemonic}
/>
<View style={styles.modalActions}>
<TouchableOpacity style={styles.btnCancel} onPress={() => setImportWalletModalVisible(false)}><Text>Cancel</Text></TouchableOpacity>
<TouchableOpacity style={styles.btnConfirm} onPress={handleImportWallet}><Text style={{color:'white'}}>Import</Text></TouchableOpacity>
</View>
</View>
</View>
</Modal>
</SafeAreaView>
);
// Show loading while checking wallet state or redirecting
if (!selectedAccount && accounts.length === 0) {
return (
<SafeAreaView style={styles.container} testID="wallet-redirecting">
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<Text style={styles.loadingText}>Loading wallet...</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<SafeAreaView style={styles.container} testID="wallet-screen">
<StatusBar barStyle="dark-content" />
{/* Top Header with Back Button */}
<View style={styles.topHeader} testID="wallet-top-header">
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton} testID="wallet-back-button">
<Text style={styles.backButtonText}> Back</Text>
</TouchableOpacity>
<Text style={styles.topHeaderTitle}>Wallet</Text>
<TouchableOpacity onPress={() => setNetworkSelectorVisible(true)} testID="wallet-network-button">
<Text style={styles.networkBadge}>🌐 {NETWORKS[currentNetwork].displayName}</Text>
</TouchableOpacity>
</View>
<ScrollView
style={styles.scrollContent}
refreshControl={<RefreshControl refreshing={isLoadingBalances} onRefresh={fetchData} />}
showsVerticalScrollIndicator={false}
testID="wallet-scroll-view"
>
{/* Header */}
<View style={styles.headerContainer}>
<Text style={styles.walletTitle}>pezkuwi wallet</Text>
<TouchableOpacity onPress={() => setNetworkSelectorVisible(true)}>
<Text style={styles.networkBadge}>🌐 {NETWORKS[currentNetwork].displayName}</Text>
{/* Wallet Selector Row */}
<View style={styles.walletSelectorRow}>
<TouchableOpacity
style={styles.walletSelector}
onPress={() => setWalletSelectorVisible(true)}
testID="wallet-selector-button"
>
<View style={styles.walletSelectorInfo}>
<Text style={styles.walletSelectorName}>{selectedAccount?.name || 'Wallet'}</Text>
<Text style={styles.walletSelectorAddress} numberOfLines={1}>
{selectedAccount?.address ? `${selectedAccount.address.slice(0, 8)}...${selectedAccount.address.slice(-6)}` : ''}
</Text>
</View>
<Text style={styles.walletSelectorArrow}></Text>
</TouchableOpacity>
<View style={styles.walletHeaderButtons}>
<TouchableOpacity
style={styles.addWalletButton}
onPress={() => navigation.navigate('WalletSetup')}
testID="add-wallet-button"
>
<Text style={styles.addWalletIcon}>+</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.scanButton}
onPress={() => showAlert('Scan', 'QR Scanner coming soon')}
testID="wallet-scan-button"
>
<Text style={styles.scanIcon}></Text>
</TouchableOpacity>
</View>
</View>
{/* Main Token Cards - HEZ and PEZ side by side */}
<View style={styles.mainTokensRow}>
{/* HEZ Card */}
<TouchableOpacity style={styles.mainTokenCard} onPress={() => handleTokenPress(tokens[0])}>
<Image source={hezLogo} style={styles.mainTokenLogo} resizeMode="contain" />
<View style={styles.mainTokenLogoContainer}>
<HezTokenLogo size={56} />
</View>
<Text style={styles.mainTokenSymbol}>HEZ</Text>
<Text style={styles.mainTokenBalance}>{balances.HEZ}</Text>
<Text style={styles.mainTokenSubtitle}>Hemuwelet Token</Text>
<Text style={styles.mainTokenSubtitle}>Welati Coin</Text>
</TouchableOpacity>
{/* PEZ Card */}
<TouchableOpacity style={styles.mainTokenCard} onPress={() => handleTokenPress(tokens[1])}>
<Image source={pezLogo} style={styles.mainTokenLogo} resizeMode="contain" />
<View style={styles.mainTokenLogoContainer}>
<PezTokenLogo size={56} />
</View>
<Text style={styles.mainTokenSymbol}>PEZ</Text>
<Text style={styles.mainTokenBalance}>{balances.PEZ}</Text>
<Text style={styles.mainTokenSubtitle}>Pezkunel Token</Text>
<Text style={styles.mainTokenSubtitle}>Pezkuwichain Token</Text>
</TouchableOpacity>
</View>
{/* Action Buttons Grid - 2x4 */}
{/* Action Buttons Grid - 1x4 */}
<View style={styles.actionsGrid}>
{/* Row 1 */}
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#22C55E'}]} onPress={handleSend}>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionLabel}>Send</Text>
@@ -494,36 +495,15 @@ const WalletScreen: React.FC = () => {
<Text style={styles.actionLabel}>Receive</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#A855F7'}]} onPress={() => Alert.alert('Scan', 'QR Scanner coming soon')}>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionLabel}>Scan</Text>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#6B7280'}]} onPress={() => navigation.navigate('Swap')}>
<Text style={styles.actionIcon}>🔄</Text>
<Text style={styles.actionLabel}>Swap</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#6B7280'}]} onPress={() => Alert.alert('P2P', 'Navigate to P2P Platform')}>
<Text style={styles.actionIcon}>👥</Text>
<Text style={styles.actionLabel}>P2P</Text>
</TouchableOpacity>
{/* Row 2 */}
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#EF4444'}]} onPress={() => Alert.alert('Vote', 'Navigate to Governance')}>
<Text style={styles.actionIcon}>🗳</Text>
<Text style={styles.actionLabel}>Vote</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#F59E0B'}]} onPress={() => Alert.alert('Dapps', 'Navigate to Apps')}>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionLabel}>Dapps</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#10B981'}]} onPress={() => Alert.alert('Staking', 'Navigate to Staking')}>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#10B981'}]} onPress={() => showAlert('Staking', 'Navigate to Staking')}>
<Text style={styles.actionIcon}>🥩</Text>
<Text style={styles.actionLabel}>Staking</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#8B5CF6'}]} onPress={() => setNetworkSelectorVisible(true)}>
<Text style={styles.actionIcon}>🔗</Text>
<Text style={styles.actionLabel}>Connect</Text>
</TouchableOpacity>
</View>
{/* Tokens List */}
@@ -630,7 +610,7 @@ const WalletScreen: React.FC = () => {
<View style={styles.modalActions}>
<TouchableOpacity style={styles.btnCancel} onPress={() => {
Clipboard.setString(userMnemonic);
Alert.alert('Copied', 'Mnemonic copied to clipboard');
showAlert('Copied', 'Mnemonic copied to clipboard');
}}>
<Text>📋 Copy</Text>
</TouchableOpacity>
@@ -642,6 +622,103 @@ const WalletScreen: React.FC = () => {
</View>
</Modal>
{/* Wallet Selector Modal */}
<Modal visible={walletSelectorVisible} transparent animationType="slide" onRequestClose={() => setWalletSelectorVisible(false)}>
<View style={styles.modalOverlay}>
<View style={styles.modalCard}>
<Text style={styles.modalHeader}>👛 My Wallets</Text>
<Text style={{color: '#666', fontSize: 12, marginBottom: 16, textAlign: 'center'}}>
Select a wallet or create a new one
</Text>
{/* Wallet List */}
{accounts.map((account) => {
const isSelected = account.address === selectedAccount?.address;
return (
<View key={account.address} style={styles.walletOptionRow}>
<TouchableOpacity
style={[
styles.walletOption,
isSelected && styles.walletOptionSelected,
{flex: 1, marginBottom: 0}
]}
onPress={() => {
setSelectedAccount(account);
setWalletSelectorVisible(false);
}}
>
<View style={styles.walletOptionIcon}>
<Text style={{fontSize: 24}}>👛</Text>
</View>
<View style={{flex: 1}}>
<Text style={[styles.walletOptionName, isSelected && {color: KurdistanColors.kesk}]}>
{account.name}
</Text>
<Text style={styles.walletOptionAddress} numberOfLines={1}>
{account.address.slice(0, 12)}...{account.address.slice(-8)}
</Text>
</View>
{isSelected && <Text style={{fontSize: 20, color: KurdistanColors.kesk}}></Text>}
</TouchableOpacity>
<TouchableOpacity
style={styles.deleteWalletButton}
onPress={async () => {
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<boolean>((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');
}
}
}
}}
>
<Text style={styles.deleteWalletIcon}>🗑</Text>
</TouchableOpacity>
</View>
);
})}
{/* Add New Wallet Button */}
<TouchableOpacity
style={styles.addNewWalletOption}
onPress={() => {
setWalletSelectorVisible(false);
navigation.navigate('WalletSetup');
}}
>
<View style={styles.addNewWalletIcon}>
<Text style={{fontSize: 24, color: KurdistanColors.kesk}}>+</Text>
</View>
<Text style={styles.addNewWalletText}>Add New Wallet</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btnConfirm} onPress={() => setWalletSelectorVisible(false)}>
<Text style={{color:'white'}}>Close</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
{/* Network Selector Modal */}
<Modal visible={networkSelectorVisible} transparent animationType="slide" onRequestClose={() => setNetworkSelectorVisible(false)}>
<View style={styles.modalOverlay}>
@@ -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;
+820
View File
@@ -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<any>();
const { createWallet, importWallet, connectWallet, isReady } = usePezkuwi();
const [step, setStep] = useState<SetupStep>('choice');
const [mnemonic, setMnemonic] = useState<string[]>([]);
const [walletName, setWalletName] = useState('');
const [importMnemonic, setImportMnemonic] = useState('');
const [verificationIndices, setVerificationIndices] = useState<number[]>([]);
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 = () => (
<View style={styles.stepContainer} testID="wallet-setup-choice">
<View style={styles.iconContainer}>
<Text style={styles.mainIcon}>👛</Text>
</View>
<Text style={styles.title}>Set Up Your Wallet</Text>
<Text style={styles.subtitle}>
Create a new wallet or import an existing one using your recovery phrase
</Text>
<View style={styles.choiceButtons}>
<TouchableOpacity
style={styles.choiceButton}
onPress={handleCreateNew}
testID="wallet-setup-create-button"
>
<LinearGradient
colors={[KurdistanColors.kesk, '#008f43']}
style={styles.choiceButtonGradient}
>
<Text style={styles.choiceButtonIcon}></Text>
<Text style={styles.choiceButtonTitle}>Create New Wallet</Text>
<Text style={styles.choiceButtonSubtitle}>
Generate a new recovery phrase
</Text>
</LinearGradient>
</TouchableOpacity>
<TouchableOpacity
style={styles.choiceButton}
onPress={handleImport}
testID="wallet-setup-import-button"
>
<View style={styles.choiceButtonOutline}>
<Text style={styles.choiceButtonIcon}>📥</Text>
<Text style={[styles.choiceButtonTitle, { color: KurdistanColors.reş }]}>
Import Existing Wallet
</Text>
<Text style={[styles.choiceButtonSubtitle, { color: '#666' }]}>
Use your 12 or 24 word phrase
</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
const renderCreateShowStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-show-seed">
<Text style={styles.title}>Your Recovery Phrase</Text>
<Text style={styles.subtitle}>
Write down these 12 words in order and keep them safe. This is the only way to recover your wallet.
</Text>
<View style={styles.warningBox}>
<Text style={styles.warningIcon}></Text>
<Text style={styles.warningText}>
Never share your recovery phrase with anyone. Anyone with these words can access your funds.
</Text>
</View>
<View style={styles.mnemonicGrid} testID="mnemonic-grid">
{mnemonic.map((word, index) => (
<View key={index} style={styles.wordCard}>
<Text style={styles.wordNumber}>{index + 1}</Text>
<Text style={styles.wordText}>{word}</Text>
</View>
))}
</View>
<TouchableOpacity
style={styles.primaryButton}
onPress={handleMnemonicConfirmed}
testID="wallet-setup-continue-button"
>
<Text style={styles.primaryButtonText}>I've Written It Down</Text>
</TouchableOpacity>
</View>
);
const renderCreateVerifyStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-verify-seed">
<Text style={styles.title}>Verify Your Phrase</Text>
<Text style={styles.subtitle}>
Select the correct words to verify you've saved your recovery phrase
</Text>
<View style={styles.verificationContainer}>
{verificationIndices.map((wordIndex) => (
<View key={wordIndex} style={styles.verificationItem}>
<Text style={styles.verificationLabel}>Word #{wordIndex + 1}</Text>
<View style={styles.verificationOptions}>
{getShuffledOptions(mnemonic[wordIndex]).map((option, optIdx) => (
<TouchableOpacity
key={optIdx}
style={[
styles.verificationOption,
selectedWords[wordIndex] === option && styles.verificationOptionSelected,
selectedWords[wordIndex] === option &&
selectedWords[wordIndex] === mnemonic[wordIndex] && styles.verificationOptionCorrect,
]}
onPress={() => handleVerifyWord(wordIndex, option)}
testID={`verify-option-${wordIndex}-${optIdx}`}
>
<Text style={[
styles.verificationOptionText,
selectedWords[wordIndex] === option && styles.verificationOptionTextSelected,
]}>
{option}
</Text>
</TouchableOpacity>
))}
</View>
</View>
))}
</View>
<TouchableOpacity
style={[
styles.primaryButton,
!Object.keys(selectedWords).length && styles.primaryButtonDisabled
]}
onPress={handleVerificationComplete}
disabled={Object.keys(selectedWords).length !== 3}
testID="wallet-setup-verify-button"
>
<Text style={styles.primaryButtonText}>Verify & Continue</Text>
</TouchableOpacity>
</View>
);
const renderImportStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-import">
<Text style={styles.title}>Import Wallet</Text>
<Text style={styles.subtitle}>
Enter your 12 or 24 word recovery phrase, or a dev URI like //Alice
</Text>
<View style={styles.importInputContainer}>
<TextInput
style={styles.importInput}
placeholder="Enter recovery phrase or //Alice..."
placeholderTextColor="#999"
multiline
numberOfLines={4}
value={importMnemonic}
onChangeText={setImportMnemonic}
autoCapitalize="none"
autoCorrect={false}
testID="wallet-import-input"
/>
<Text style={styles.importHint}>
Mnemonic: separate words with space | Dev URI: //Alice, //Bob, etc.
</Text>
</View>
<TouchableOpacity
style={[
styles.primaryButton,
!importMnemonic.trim() && styles.primaryButtonDisabled
]}
onPress={handleImportWallet}
disabled={!importMnemonic.trim()}
testID="wallet-import-continue-button"
>
<Text style={styles.primaryButtonText}>Continue</Text>
</TouchableOpacity>
</View>
);
const renderWalletNameStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-name">
<Text style={styles.title}>Name Your Wallet</Text>
<Text style={styles.subtitle}>
Give your wallet a name to easily identify it
</Text>
<View style={styles.nameInputContainer}>
<TextInput
style={styles.nameInput}
placeholder="e.g., My Main Wallet"
placeholderTextColor="#999"
value={walletName}
onChangeText={setWalletName}
autoCapitalize="words"
maxLength={30}
testID="wallet-name-input"
/>
</View>
<TouchableOpacity
style={[
styles.primaryButton,
(!walletName.trim() || isLoading) && styles.primaryButtonDisabled
]}
onPress={isCreateFlow ? handleCreateWallet : handleImportComplete}
disabled={!walletName.trim() || isLoading}
testID="wallet-setup-finish-button"
>
{isLoading ? (
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.primaryButtonText}>
{isCreateFlow ? 'Create Wallet' : 'Import Wallet'}
</Text>
)}
</TouchableOpacity>
</View>
);
const renderSuccessStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-success">
<View style={styles.successIconContainer}>
<Text style={styles.successIcon}></Text>
</View>
<Text style={styles.title}>Wallet Created!</Text>
<Text style={styles.subtitle}>
Your wallet is ready to use. You can now send and receive tokens.
</Text>
<View style={styles.addressBox}>
<Text style={styles.addressLabel}>Your Wallet Address</Text>
<Text style={styles.addressText} numberOfLines={1} ellipsizeMode="middle">
{createdAddress}
</Text>
</View>
<TouchableOpacity
style={styles.primaryButton}
onPress={handleFinish}
testID="wallet-setup-done-button"
>
<Text style={styles.primaryButtonText}>Go to Wallet</Text>
</TouchableOpacity>
</View>
);
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 (
<SafeAreaView style={styles.container} testID="wallet-setup-screen">
{/* Header */}
{step !== 'choice' && step !== 'success' && (
<View style={styles.header}>
<TouchableOpacity onPress={handleBack} style={styles.backButton} testID="wallet-setup-back">
<Text style={styles.backButtonText}> Back</Text>
</TouchableOpacity>
<View style={styles.progressContainer}>
{['create-show', 'create-verify', 'wallet-name'].includes(step) && isCreateFlow && (
<>
<View style={[styles.progressDot, step === 'create-show' && styles.progressDotActive]} />
<View style={[styles.progressDot, step === 'create-verify' && styles.progressDotActive]} />
<View style={[styles.progressDot, step === 'wallet-name' && styles.progressDotActive]} />
</>
)}
{['import', 'wallet-name'].includes(step) && !isCreateFlow && (
<>
<View style={[styles.progressDot, step === 'import' && styles.progressDotActive]} />
<View style={[styles.progressDot, step === 'wallet-name' && styles.progressDotActive]} />
</>
)}
</View>
<View style={styles.headerSpacer} />
</View>
)}
{/* Close button on choice screen */}
{step === 'choice' && (
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.closeButton} testID="wallet-setup-close">
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
)}
<ScrollView
style={styles.content}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{renderStep()}
</ScrollView>
</SafeAreaView>
);
};
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;
-2
View File
@@ -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<WelcomeScreenProps> = ({ onContinue }) => {
const { t } = useTranslation();
const [agreed, setAgreed] = useState(false);
const [privacyModalVisible, setPrivacyModalVisible] = useState(false);
const [termsModalVisible, setTermsModalVisible] = useState(false);
@@ -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 = () => (
<LanguageProvider>
<PezkuwiProvider>
<BeCitizenScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<BeCitizenScreen />
</PezkuwiProvider>
);
describe('BeCitizenScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<PezkuwiProvider>
<EducationScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<EducationScreen />
</PezkuwiProvider>
);
describe('EducationScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<AuthProvider>
<ForumScreen />
</AuthProvider>
</LanguageProvider>
<AuthProvider>
<ForumScreen />
</AuthProvider>
);
describe('ForumScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<PezkuwiProvider>
<GovernanceScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<GovernanceScreen />
</PezkuwiProvider>
);
describe('GovernanceScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<BiometricAuthProvider>
<LockScreen />
</BiometricAuthProvider>
</LanguageProvider>
<BiometricAuthProvider>
<LockScreen />
</BiometricAuthProvider>
);
describe('LockScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<PezkuwiProvider>
<NFTGalleryScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<NFTGalleryScreen />
</PezkuwiProvider>
);
describe('NFTGalleryScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<PezkuwiProvider>
<P2PScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<P2PScreen />
</PezkuwiProvider>
);
describe('P2PScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<ProfileScreen />
</LanguageProvider>
<ProfileScreen />
);
describe('ProfileScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<PezkuwiProvider>
<ReferralScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<ReferralScreen />
</PezkuwiProvider>
);
describe('ReferralScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<BiometricAuthProvider>
<SecurityScreen />
</BiometricAuthProvider>
</LanguageProvider>
<BiometricAuthProvider>
<SecurityScreen />
</BiometricAuthProvider>
);
describe('SecurityScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<AuthProvider>
<SignInScreen />
</AuthProvider>
</LanguageProvider>
<AuthProvider>
<SignInScreen />
</AuthProvider>
);
describe('SignInScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<AuthProvider>
<SignUpScreen />
</AuthProvider>
</LanguageProvider>
<AuthProvider>
<SignUpScreen />
</AuthProvider>
);
describe('SignUpScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<PezkuwiProvider>
<StakingScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<StakingScreen />
</PezkuwiProvider>
);
describe('StakingScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<PezkuwiProvider>
<SwapScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<SwapScreen />
</PezkuwiProvider>
);
describe('SwapScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<PezkuwiProvider>
<WalletScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<WalletScreen />
</PezkuwiProvider>
);
describe('WalletScreen', () => {
@@ -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 = () => (
<LanguageProvider>
<WelcomeScreen />
</LanguageProvider>
<WelcomeScreen />
);
describe('WelcomeScreen', () => {