mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-28 03:47:59 +00:00
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -0,0 +1,2 @@
|
||||
export { default as HezTokenLogo } from './HezTokenLogo';
|
||||
export { default as PezTokenLogo } from './PezTokenLogo';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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": "اختياري"
|
||||
}
|
||||
}
|
||||
@@ -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": "ئیختیاری"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "اختیاری"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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ı"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user