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

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

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

Reality check: UI complete with mock data, converting to production one-by-one
This commit is contained in:
2026-01-15 05:08:21 +03:00
parent 5d293cc954
commit f2e70a8150
110 changed files with 11157 additions and 3260 deletions
-64
View File
@@ -1,64 +0,0 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { I18nManager } from 'react-native';
import { isRTL, languages } from '../i18n';
import i18n from '../i18n';
// Language is set at build time via environment variable
const BUILD_LANGUAGE = process.env.EXPO_PUBLIC_DEFAULT_LANGUAGE || 'en';
interface Language {
code: string;
name: string;
nativeName: string;
rtl: boolean;
}
interface LanguageContextType {
currentLanguage: string;
isRTL: boolean;
hasSelectedLanguage: boolean;
availableLanguages: Language[];
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
export const LanguageProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
// Language is fixed at build time - no runtime switching
const [currentLanguage] = useState(BUILD_LANGUAGE);
const [currentIsRTL] = useState(isRTL(BUILD_LANGUAGE));
useEffect(() => {
// Initialize i18n with build-time language
i18n.changeLanguage(BUILD_LANGUAGE);
// Set RTL if needed
const isRTLLanguage = ['ar', 'ckb', 'fa'].includes(BUILD_LANGUAGE);
I18nManager.allowRTL(isRTLLanguage);
I18nManager.forceRTL(isRTLLanguage);
if (__DEV__) {
console.log(`[LanguageContext] Build language: ${BUILD_LANGUAGE}, RTL: ${isRTLLanguage}`);
}
}, []);
return (
<LanguageContext.Provider
value={{
currentLanguage,
isRTL: currentIsRTL,
hasSelectedLanguage: true, // Always true - language pre-selected at build time
availableLanguages: languages,
}}
>
{children}
</LanguageContext.Provider>
);
};
export const useLanguage = (): LanguageContextType => {
const context = useContext(LanguageContext);
if (!context) {
throw new Error('useLanguage must be used within LanguageProvider');
}
return context;
};
+114 -23
View File
@@ -1,4 +1,5 @@
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { Platform } from 'react-native';
import { Keyring } from '@pezkuwi/keyring';
import { KeyringPair } from '@pezkuwi/keyring/types';
import { ApiPromise, WsProvider } from '@pezkuwi/api';
@@ -7,6 +8,34 @@ import * as SecureStore from 'expo-secure-store';
import { cryptoWaitReady, mnemonicGenerate } from '@pezkuwi/util-crypto';
import { ENV } from '../config/environment';
// Secure storage helper - uses SecureStore on native, AsyncStorage on web (with warning)
const secureStorage = {
setItem: async (key: string, value: string): Promise<void> => {
if (Platform.OS === 'web') {
// WARNING: AsyncStorage is NOT secure for storing seeds on web
// In production, consider using Web Crypto API or server-side storage
if (__DEV__) console.warn('[SecureStorage] Using AsyncStorage on web - NOT SECURE for production');
await AsyncStorage.setItem(key, value);
} else {
await SecureStore.setItemAsync(key, value);
}
},
getItem: async (key: string): Promise<string | null> => {
if (Platform.OS === 'web') {
return await AsyncStorage.getItem(key);
} else {
return await SecureStore.getItemAsync(key);
}
},
removeItem: async (key: string): Promise<void> => {
if (Platform.OS === 'web') {
await AsyncStorage.removeItem(key);
} else {
await SecureStore.deleteItemAsync(key);
}
},
};
interface Account {
address: string;
name: string;
@@ -15,14 +44,14 @@ interface Account {
};
}
export type NetworkType = 'pezkuwi' | 'dicle' | 'zagros' | 'bizinikiwi';
export type NetworkType = 'pezkuwi' | 'dicle' | 'zagros' | 'bizinikiwi' | 'zombienet';
export interface NetworkConfig {
name: string;
displayName: string;
rpcEndpoint: string;
ss58Format: number;
type: 'mainnet' | 'testnet' | 'canary';
type: 'mainnet' | 'testnet' | 'canary' | 'dev';
}
export const NETWORKS: Record<NetworkType, NetworkConfig> = {
@@ -54,6 +83,13 @@ export const NETWORKS: Record<NetworkType, NetworkConfig> = {
ss58Format: 42,
type: 'testnet',
},
zombienet: {
name: 'zombienet',
displayName: 'Zombienet Dev (Alice/Bob)',
rpcEndpoint: 'wss://zombienet-rpc.pezkuwichain.io',
ss58Format: 42,
type: 'dev',
},
};
interface PezkuwiContextType {
@@ -73,6 +109,7 @@ interface PezkuwiContextType {
disconnectWallet: () => void;
createWallet: (name: string, mnemonic?: string) => Promise<{ address: string; mnemonic: string }>;
importWallet: (name: string, mnemonic: string) => Promise<{ address: string }>;
deleteWallet: (address: string) => Promise<void>;
getKeyPair: (address: string) => Promise<KeyringPair | null>;
signMessage: (address: string, message: string) => Promise<string | null>;
error: string | null;
@@ -131,7 +168,14 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({ children }) =>
const provider = new WsProvider(networkConfig.rpcEndpoint);
console.log('📡 [Pezkuwi] WsProvider created, creating API...');
const newApi = await ApiPromise.create({ provider });
console.log('✅ [Pezkuwi] API created successfully');
// Set SS58 format for address encoding/decoding
newApi.registry.setChainProperties(
newApi.registry.createType('ChainProperties', {
ss58Format: networkConfig.ss58Format,
})
);
console.log(`✅ [Pezkuwi] API created with SS58 format: ${networkConfig.ss58Format}`);
if (isSubscribed) {
setApi(newApi);
@@ -256,9 +300,9 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({ children }) =>
setAccounts(updatedAccounts);
await AsyncStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(updatedAccounts));
// SECURITY: Store encrypted seed in SecureStore (hardware-backed storage)
// SECURITY: Store encrypted seed in secure storage (hardware-backed on native)
const seedKey = `pezkuwi_seed_${pair.address}`;
await SecureStore.setItemAsync(seedKey, mnemonicPhrase);
await secureStorage.setItem(seedKey, mnemonicPhrase);
if (__DEV__) console.log('[Pezkuwi] Wallet created:', pair.address);
@@ -266,24 +310,33 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({ children }) =>
address: pair.address,
mnemonic: mnemonicPhrase,
};
} catch (err) {
if (__DEV__) console.error('[Pezkuwi] Failed to create wallet:', err);
throw new Error('Failed to create wallet');
} catch (err: any) {
if (__DEV__) {
console.error('[Pezkuwi] Failed to create wallet:', err);
console.error('[Pezkuwi] Error message:', err?.message);
console.error('[Pezkuwi] Error stack:', err?.stack);
}
throw new Error(err?.message || 'Failed to create wallet');
}
};
// Import existing wallet from mnemonic
// Import existing wallet from mnemonic or dev URI (like //Alice)
const importWallet = async (
name: string,
mnemonic: string
seedOrUri: string
): Promise<{ address: string }> => {
if (!keyring) {
throw new Error('Keyring not initialized');
}
try {
// Create account from mnemonic
const pair = keyring.addFromMnemonic(mnemonic.trim(), { name });
const trimmedInput = seedOrUri.trim();
const isDevUri = trimmedInput.startsWith('//');
// Create account from URI or mnemonic
const pair = isDevUri
? keyring.addFromUri(trimmedInput, { name })
: keyring.addFromMnemonic(trimmedInput, { name });
// Check if account already exists
if (accounts.some(a => a.address === pair.address)) {
@@ -301,16 +354,49 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({ children }) =>
setAccounts(updatedAccounts);
await AsyncStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(updatedAccounts));
// Store seed securely
// Store seed/URI securely
const seedKey = `pezkuwi_seed_${pair.address}`;
await SecureStore.setItemAsync(seedKey, mnemonic.trim());
await secureStorage.setItem(seedKey, trimmedInput);
if (__DEV__) console.log('[Pezkuwi] Wallet imported:', pair.address);
if (__DEV__) console.log('[Pezkuwi] Wallet imported:', pair.address, isDevUri ? '(dev URI)' : '(mnemonic)');
return { address: pair.address };
} catch (err) {
if (__DEV__) console.error('[Pezkuwi] Failed to import wallet:', err);
throw err;
} catch (err: any) {
if (__DEV__) {
console.error('[Pezkuwi] Failed to import wallet:', err);
console.error('[Pezkuwi] Error message:', err?.message);
}
throw new Error(err?.message || 'Failed to import wallet');
}
};
// Delete a wallet
const deleteWallet = async (address: string): Promise<void> => {
try {
// Remove from accounts list
const updatedAccounts = accounts.filter(a => a.address !== address);
setAccounts(updatedAccounts);
await AsyncStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(updatedAccounts));
// Remove seed from secure storage
const seedKey = `pezkuwi_seed_${address}`;
await secureStorage.removeItem(seedKey);
// If deleted account was selected, select another one
if (selectedAccount?.address === address) {
if (updatedAccounts.length > 0) {
setSelectedAccount(updatedAccounts[0]);
await AsyncStorage.setItem(SELECTED_ACCOUNT_KEY, updatedAccounts[0].address);
} else {
setSelectedAccount(null);
await AsyncStorage.removeItem(SELECTED_ACCOUNT_KEY);
}
}
if (__DEV__) console.log('[Pezkuwi] Wallet deleted:', address);
} catch (err: any) {
if (__DEV__) console.error('[Pezkuwi] Failed to delete wallet:', err);
throw new Error(err?.message || 'Failed to delete wallet');
}
};
@@ -321,17 +407,21 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({ children }) =>
}
try {
// SECURITY: Load seed from SecureStore (encrypted storage)
// SECURITY: Load seed/URI from secure storage (encrypted on native)
const seedKey = `pezkuwi_seed_${address}`;
const mnemonic = await SecureStore.getItemAsync(seedKey);
const seedOrUri = await secureStorage.getItem(seedKey);
if (!mnemonic) {
if (!seedOrUri) {
if (__DEV__) console.error('[Pezkuwi] No seed found for address:', address);
return null;
}
// Recreate keypair from mnemonic
const pair = keyring.addFromMnemonic(mnemonic);
// Recreate keypair from URI or mnemonic
const isDevUri = seedOrUri.startsWith('//');
const pair = isDevUri
? keyring.addFromUri(seedOrUri)
: keyring.addFromMnemonic(seedOrUri);
return pair;
} catch (err) {
if (__DEV__) console.error('[Pezkuwi] Failed to get keypair:', err);
@@ -431,6 +521,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({ children }) =>
disconnectWallet,
createWallet,
importWallet,
deleteWallet,
getKeyPair,
signMessage,
error,
@@ -1,105 +0,0 @@
import React from 'react';
import { renderHook, act } from '@testing-library/react-native';
import { LanguageProvider, useLanguage } from '../LanguageContext';
// Mock the i18n module relative to src/
jest.mock('../../i18n', () => ({
saveLanguage: jest.fn(() => Promise.resolve()),
getCurrentLanguage: jest.fn(() => 'en'),
isRTL: jest.fn((code?: string) => {
const testCode = code || 'en';
return ['ckb', 'ar', 'fa'].includes(testCode);
}),
LANGUAGE_KEY: '@language',
languages: [
{ code: 'en', name: 'English', nativeName: 'English', rtl: false },
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe', rtl: false },
{ code: 'kmr', name: 'Kurdish Kurmanji', nativeName: 'Kurmancî', rtl: false },
{ code: 'ckb', name: 'Kurdish Sorani', nativeName: 'سۆرانی', rtl: true },
{ code: 'ar', name: 'Arabic', nativeName: 'العربية', rtl: true },
{ code: 'fa', name: 'Persian', nativeName: 'فارسی', rtl: true },
],
}));
// Wrapper for provider
const wrapper = ({ children }: { children: React.ReactNode }) => (
<LanguageProvider>{children}</LanguageProvider>
);
describe('LanguageContext', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should provide language context', () => {
const { result } = renderHook(() => useLanguage(), { wrapper });
expect(result.current).toBeDefined();
expect(result.current.currentLanguage).toBe('en');
});
it('should change language', async () => {
const { result } = renderHook(() => useLanguage(), { wrapper });
await act(async () => {
await result.current.changeLanguage('kmr');
});
expect(result.current.currentLanguage).toBe('kmr');
});
it('should provide available languages', () => {
const { result } = renderHook(() => useLanguage(), { wrapper });
expect(result.current.availableLanguages).toBeDefined();
expect(Array.isArray(result.current.availableLanguages)).toBe(true);
expect(result.current.availableLanguages.length).toBeGreaterThan(0);
});
it('should handle RTL languages', async () => {
const { result } = renderHook(() => useLanguage(), { wrapper });
await act(async () => {
await result.current.changeLanguage('ar');
});
expect(result.current.isRTL).toBe(true);
});
it('should handle LTR languages', async () => {
const { result } = renderHook(() => useLanguage(), { wrapper });
expect(result.current.isRTL).toBe(false);
});
it('should throw error when used outside provider', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
expect(() => {
renderHook(() => useLanguage());
}).toThrow('useLanguage must be used within LanguageProvider');
spy.mockRestore();
});
it('should handle language change errors gracefully', async () => {
const { result } = renderHook(() => useLanguage(), { wrapper });
// changeLanguage should not throw but handle errors internally
await act(async () => {
await result.current.changeLanguage('en');
});
expect(result.current.currentLanguage).toBeDefined();
});
it('should persist language selection', async () => {
const { result } = renderHook(() => useLanguage(), { wrapper });
await act(async () => {
await result.current.changeLanguage('tr');
});
expect(result.current.currentLanguage).toBe('tr');
});
});