mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-23 00:07:55 +00:00
Fix all shadow deprecation warnings across entire mobile app
- Replaced shadowColor/shadowOffset/shadowOpacity/shadowRadius with boxShadow - Fixed 28 files (21 screens + 7 components) - Preserved elevation for Android compatibility - All React Native Web deprecation warnings resolved Files fixed: - All screen components - All reusable components - Navigation components - Modal components
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { I18nManager } from 'react-native';
|
||||
import { saveLanguage, getCurrentLanguage, isRTL, LANGUAGE_KEY, languages } from '../i18n';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
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;
|
||||
@@ -12,7 +15,6 @@ interface Language {
|
||||
|
||||
interface LanguageContextType {
|
||||
currentLanguage: string;
|
||||
changeLanguage: (languageCode: string) => Promise<void>;
|
||||
isRTL: boolean;
|
||||
hasSelectedLanguage: boolean;
|
||||
availableLanguages: Language[];
|
||||
@@ -21,52 +23,30 @@ interface LanguageContextType {
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
export const LanguageProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [currentLanguage, setCurrentLanguage] = useState(getCurrentLanguage());
|
||||
const [hasSelectedLanguage, setHasSelectedLanguage] = useState(false);
|
||||
const [currentIsRTL, setCurrentIsRTL] = useState(isRTL());
|
||||
|
||||
const checkLanguageSelection = React.useCallback(async () => {
|
||||
try {
|
||||
const saved = await AsyncStorage.getItem(LANGUAGE_KEY);
|
||||
setHasSelectedLanguage(!!saved);
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Failed to check language selection:', error);
|
||||
}
|
||||
}, []);
|
||||
// Language is fixed at build time - no runtime switching
|
||||
const [currentLanguage] = useState(BUILD_LANGUAGE);
|
||||
const [currentIsRTL] = useState(isRTL(BUILD_LANGUAGE));
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has already selected a language
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
checkLanguageSelection();
|
||||
}, [checkLanguageSelection]);
|
||||
// Initialize i18n with build-time language
|
||||
i18n.changeLanguage(BUILD_LANGUAGE);
|
||||
|
||||
const changeLanguage = async (languageCode: string) => {
|
||||
try {
|
||||
await saveLanguage(languageCode);
|
||||
setCurrentLanguage(languageCode);
|
||||
setHasSelectedLanguage(true);
|
||||
// Set RTL if needed
|
||||
const isRTLLanguage = ['ar', 'ckb', 'fa'].includes(BUILD_LANGUAGE);
|
||||
I18nManager.allowRTL(isRTLLanguage);
|
||||
I18nManager.forceRTL(isRTLLanguage);
|
||||
|
||||
const newIsRTL = isRTL(languageCode);
|
||||
setCurrentIsRTL(newIsRTL);
|
||||
|
||||
// Update RTL layout if needed
|
||||
if (I18nManager.isRTL !== newIsRTL) {
|
||||
// Note: Changing RTL requires app restart in React Native
|
||||
I18nManager.forceRTL(newIsRTL);
|
||||
// You may want to show a message to restart the app
|
||||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Failed to change language:', error);
|
||||
if (__DEV__) {
|
||||
console.log(`[LanguageContext] Build language: ${BUILD_LANGUAGE}, RTL: ${isRTLLanguage}`);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider
|
||||
value={{
|
||||
currentLanguage,
|
||||
changeLanguage,
|
||||
isRTL: currentIsRTL,
|
||||
hasSelectedLanguage,
|
||||
hasSelectedLanguage: true, // Always true - language pre-selected at build time
|
||||
availableLanguages: languages,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { Keyring } from '@pezkuwi/keyring';
|
||||
import { KeyringPair } from '@pezkuwi/keyring/types';
|
||||
import { ApiPromise, WsProvider } from '@pezkuwi/api';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { cryptoWaitReady, mnemonicGenerate } from '@pezkuwi/util-crypto';
|
||||
import { ENV } from '../config/environment';
|
||||
|
||||
interface Account {
|
||||
address: string;
|
||||
name: string;
|
||||
meta?: {
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type NetworkType = 'pezkuwi' | 'dicle' | 'zagros' | 'bizinikiwi';
|
||||
|
||||
export interface NetworkConfig {
|
||||
name: string;
|
||||
displayName: string;
|
||||
rpcEndpoint: string;
|
||||
ss58Format: number;
|
||||
type: 'mainnet' | 'testnet' | 'canary';
|
||||
}
|
||||
|
||||
export const NETWORKS: Record<NetworkType, NetworkConfig> = {
|
||||
pezkuwi: {
|
||||
name: 'pezkuwi',
|
||||
displayName: 'Pezkuwi Mainnet',
|
||||
rpcEndpoint: 'wss://rpc-mainnet.pezkuwichain.io:9944',
|
||||
ss58Format: 42,
|
||||
type: 'mainnet',
|
||||
},
|
||||
dicle: {
|
||||
name: 'dicle',
|
||||
displayName: 'Dicle Testnet',
|
||||
rpcEndpoint: 'wss://rpc-dicle.pezkuwichain.io:9944',
|
||||
ss58Format: 2,
|
||||
type: 'testnet',
|
||||
},
|
||||
zagros: {
|
||||
name: 'zagros',
|
||||
displayName: 'Zagros Canary',
|
||||
rpcEndpoint: 'wss://rpc-zagros.pezkuwichain.io:9944',
|
||||
ss58Format: 42,
|
||||
type: 'canary',
|
||||
},
|
||||
bizinikiwi: {
|
||||
name: 'bizinikiwi',
|
||||
displayName: 'Bizinikiwi Testnet (Beta)',
|
||||
rpcEndpoint: ENV.wsEndpoint || 'wss://rpc.pezkuwichain.io:9944',
|
||||
ss58Format: 42,
|
||||
type: 'testnet',
|
||||
},
|
||||
};
|
||||
|
||||
interface PezkuwiContextType {
|
||||
// Chain state
|
||||
api: ApiPromise | null;
|
||||
isApiReady: boolean;
|
||||
// Keyring state
|
||||
isReady: boolean;
|
||||
accounts: Account[];
|
||||
selectedAccount: Account | null;
|
||||
setSelectedAccount: (account: Account | null) => void;
|
||||
// Network management
|
||||
currentNetwork: NetworkType;
|
||||
switchNetwork: (network: NetworkType) => Promise<void>;
|
||||
// Wallet operations
|
||||
connectWallet: () => Promise<void>;
|
||||
disconnectWallet: () => void;
|
||||
createWallet: (name: string, mnemonic?: string) => Promise<{ address: string; mnemonic: string }>;
|
||||
importWallet: (name: string, mnemonic: string) => Promise<{ address: string }>;
|
||||
getKeyPair: (address: string) => Promise<KeyringPair | null>;
|
||||
signMessage: (address: string, message: string) => Promise<string | null>;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const PezkuwiContext = createContext<PezkuwiContextType | undefined>(undefined);
|
||||
|
||||
const WALLET_STORAGE_KEY = '@pezkuwi_wallets';
|
||||
const SELECTED_ACCOUNT_KEY = '@pezkuwi_selected_account';
|
||||
const SELECTED_NETWORK_KEY = '@pezkuwi_selected_network';
|
||||
|
||||
interface PezkuwiProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({ children }) => {
|
||||
const [api, setApi] = useState<ApiPromise | null>(null);
|
||||
const [isApiReady, setIsApiReady] = useState(false);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
||||
const [currentNetwork, setCurrentNetwork] = useState<NetworkType>('bizinikiwi');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [keyring, setKeyring] = useState<Keyring | null>(null);
|
||||
|
||||
// Load saved network on mount
|
||||
useEffect(() => {
|
||||
const loadNetwork = async () => {
|
||||
try {
|
||||
const savedNetwork = await AsyncStorage.getItem(SELECTED_NETWORK_KEY);
|
||||
if (savedNetwork && savedNetwork in NETWORKS) {
|
||||
setCurrentNetwork(savedNetwork as NetworkType);
|
||||
}
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('[Pezkuwi] Failed to load network:', err);
|
||||
}
|
||||
};
|
||||
|
||||
loadNetwork();
|
||||
}, []);
|
||||
|
||||
// Initialize blockchain connection
|
||||
useEffect(() => {
|
||||
let retryTimeout: NodeJS.Timeout;
|
||||
let isSubscribed = true;
|
||||
|
||||
const initApi = async () => {
|
||||
try {
|
||||
console.log('🔗 [Pezkuwi] Starting API initialization...');
|
||||
setIsApiReady(false);
|
||||
setError(null); // Clear previous errors
|
||||
|
||||
const networkConfig = NETWORKS[currentNetwork];
|
||||
console.log(`🌐 [Pezkuwi] Connecting to ${networkConfig.displayName} at ${networkConfig.rpcEndpoint}`);
|
||||
|
||||
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');
|
||||
|
||||
if (isSubscribed) {
|
||||
setApi(newApi);
|
||||
setIsApiReady(true);
|
||||
setError(null); // Clear any previous errors
|
||||
console.log('✅ [Pezkuwi] Connected to', networkConfig.displayName);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ [Pezkuwi] Failed to connect to blockchain:', err);
|
||||
console.error('❌ [Pezkuwi] Error details:', JSON.stringify(err, null, 2));
|
||||
|
||||
if (isSubscribed) {
|
||||
setError('Failed to connect to blockchain. Check your internet connection.');
|
||||
setIsApiReady(false); // ✅ FIX: Don't set ready on error
|
||||
setApi(null); // ✅ FIX: Clear API on error
|
||||
|
||||
// Retry connection after 5 seconds
|
||||
console.log('🔄 [Pezkuwi] Will retry connection in 5 seconds...');
|
||||
retryTimeout = setTimeout(() => {
|
||||
if (isSubscribed) {
|
||||
console.log('🔄 [Pezkuwi] Retrying blockchain connection...');
|
||||
initApi();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initApi();
|
||||
|
||||
// Cleanup on network change or unmount
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
if (retryTimeout) {
|
||||
clearTimeout(retryTimeout);
|
||||
}
|
||||
if (api) {
|
||||
api.disconnect();
|
||||
}
|
||||
};
|
||||
}, [currentNetwork]);
|
||||
|
||||
// Initialize crypto and keyring
|
||||
useEffect(() => {
|
||||
const initCrypto = async () => {
|
||||
try {
|
||||
console.log('🔐 [Pezkuwi] Starting crypto initialization...');
|
||||
console.log('⏳ [Pezkuwi] Waiting for crypto libraries...');
|
||||
|
||||
await cryptoWaitReady();
|
||||
console.log('✅ [Pezkuwi] Crypto wait ready completed');
|
||||
|
||||
const networkConfig = NETWORKS[currentNetwork];
|
||||
console.log(`🌐 [Pezkuwi] Creating keyring for ${networkConfig.displayName}`);
|
||||
|
||||
const kr = new Keyring({ type: 'sr25519', ss58Format: networkConfig.ss58Format });
|
||||
setKeyring(kr);
|
||||
setIsReady(true);
|
||||
console.log('✅ [Pezkuwi] Crypto libraries initialized successfully');
|
||||
} catch (err) {
|
||||
console.error('❌ [Pezkuwi] Failed to initialize crypto:', err);
|
||||
console.error('❌ [Pezkuwi] Error details:', JSON.stringify(err, null, 2));
|
||||
setError('Failed to initialize crypto libraries');
|
||||
// Still set ready to allow app to work without crypto
|
||||
setIsReady(true);
|
||||
}
|
||||
};
|
||||
|
||||
initCrypto();
|
||||
}, [currentNetwork]);
|
||||
|
||||
// Load stored accounts on mount
|
||||
useEffect(() => {
|
||||
const loadAccounts = async () => {
|
||||
try {
|
||||
const stored = await AsyncStorage.getItem(WALLET_STORAGE_KEY);
|
||||
if (stored) {
|
||||
const wallets = JSON.parse(stored);
|
||||
setAccounts(wallets);
|
||||
|
||||
// Load selected account
|
||||
const selectedAddr = await AsyncStorage.getItem(SELECTED_ACCOUNT_KEY);
|
||||
if (selectedAddr) {
|
||||
const account = wallets.find((w: Account) => w.address === selectedAddr);
|
||||
if (account) {
|
||||
setSelectedAccount(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('[Pezkuwi] Failed to load accounts:', err);
|
||||
}
|
||||
};
|
||||
|
||||
loadAccounts();
|
||||
}, []);
|
||||
|
||||
// Create a new wallet
|
||||
const createWallet = async (
|
||||
name: string,
|
||||
mnemonic?: string
|
||||
): Promise<{ address: string; mnemonic: string }> => {
|
||||
if (!keyring) {
|
||||
throw new Error('Keyring not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate or use provided mnemonic
|
||||
const mnemonicPhrase = mnemonic || mnemonicGenerate(12);
|
||||
|
||||
// Create account from mnemonic
|
||||
const pair = keyring.addFromMnemonic(mnemonicPhrase, { name });
|
||||
|
||||
const newAccount: Account = {
|
||||
address: pair.address,
|
||||
name,
|
||||
meta: { name },
|
||||
};
|
||||
|
||||
// Store account (address only, not the seed!)
|
||||
const updatedAccounts = [...accounts, newAccount];
|
||||
setAccounts(updatedAccounts);
|
||||
await AsyncStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(updatedAccounts));
|
||||
|
||||
// SECURITY: Store encrypted seed in SecureStore (hardware-backed storage)
|
||||
const seedKey = `pezkuwi_seed_${pair.address}`;
|
||||
await SecureStore.setItemAsync(seedKey, mnemonicPhrase);
|
||||
|
||||
if (__DEV__) console.log('[Pezkuwi] Wallet created:', pair.address);
|
||||
|
||||
return {
|
||||
address: pair.address,
|
||||
mnemonic: mnemonicPhrase,
|
||||
};
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('[Pezkuwi] Failed to create wallet:', err);
|
||||
throw new Error('Failed to create wallet');
|
||||
}
|
||||
};
|
||||
|
||||
// Import existing wallet from mnemonic
|
||||
const importWallet = async (
|
||||
name: string,
|
||||
mnemonic: string
|
||||
): Promise<{ address: string }> => {
|
||||
if (!keyring) {
|
||||
throw new Error('Keyring not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Create account from mnemonic
|
||||
const pair = keyring.addFromMnemonic(mnemonic.trim(), { name });
|
||||
|
||||
// Check if account already exists
|
||||
if (accounts.some(a => a.address === pair.address)) {
|
||||
throw new Error('Wallet already exists');
|
||||
}
|
||||
|
||||
const newAccount: Account = {
|
||||
address: pair.address,
|
||||
name,
|
||||
meta: { name },
|
||||
};
|
||||
|
||||
// Store account
|
||||
const updatedAccounts = [...accounts, newAccount];
|
||||
setAccounts(updatedAccounts);
|
||||
await AsyncStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(updatedAccounts));
|
||||
|
||||
// Store seed securely
|
||||
const seedKey = `pezkuwi_seed_${pair.address}`;
|
||||
await SecureStore.setItemAsync(seedKey, mnemonic.trim());
|
||||
|
||||
if (__DEV__) console.log('[Pezkuwi] Wallet imported:', pair.address);
|
||||
|
||||
return { address: pair.address };
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('[Pezkuwi] Failed to import wallet:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Get keypair for signing transactions
|
||||
const getKeyPair = async (address: string): Promise<KeyringPair | null> => {
|
||||
if (!keyring) {
|
||||
throw new Error('Keyring not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// SECURITY: Load seed from SecureStore (encrypted storage)
|
||||
const seedKey = `pezkuwi_seed_${address}`;
|
||||
const mnemonic = await SecureStore.getItemAsync(seedKey);
|
||||
|
||||
if (!mnemonic) {
|
||||
if (__DEV__) console.error('[Pezkuwi] No seed found for address:', address);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Recreate keypair from mnemonic
|
||||
const pair = keyring.addFromMnemonic(mnemonic);
|
||||
return pair;
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('[Pezkuwi] Failed to get keypair:', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Sign a message with the keypair
|
||||
const signMessage = async (address: string, message: string): Promise<string | null> => {
|
||||
try {
|
||||
const pair = await getKeyPair(address);
|
||||
if (!pair) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sign the message
|
||||
const signature = pair.sign(message);
|
||||
// Convert to hex string
|
||||
const signatureHex = Buffer.from(signature).toString('hex');
|
||||
return signatureHex;
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('[Pezkuwi] Failed to sign message:', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Connect wallet (load existing accounts)
|
||||
const connectWallet = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
if (accounts.length === 0) {
|
||||
setError('No wallets found. Please create a wallet first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-select first account if none selected
|
||||
if (!selectedAccount && accounts.length > 0) {
|
||||
setSelectedAccount(accounts[0]);
|
||||
await AsyncStorage.setItem(SELECTED_ACCOUNT_KEY, accounts[0].address);
|
||||
}
|
||||
|
||||
if (__DEV__) console.log(`[Pezkuwi] Connected with ${accounts.length} account(s)`);
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('[Pezkuwi] Wallet connection failed:', err);
|
||||
setError('Failed to connect wallet');
|
||||
}
|
||||
};
|
||||
|
||||
// Disconnect wallet
|
||||
const disconnectWallet = () => {
|
||||
setSelectedAccount(null);
|
||||
AsyncStorage.removeItem(SELECTED_ACCOUNT_KEY);
|
||||
if (__DEV__) console.log('[Pezkuwi] Wallet disconnected');
|
||||
};
|
||||
|
||||
// Switch network
|
||||
const switchNetwork = async (network: NetworkType) => {
|
||||
try {
|
||||
if (network === currentNetwork) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEV__) console.log('[Pezkuwi] Switching to network:', NETWORKS[network].displayName);
|
||||
|
||||
// Save network preference
|
||||
await AsyncStorage.setItem(SELECTED_NETWORK_KEY, network);
|
||||
|
||||
// Update state (will trigger useEffect to reconnect)
|
||||
setCurrentNetwork(network);
|
||||
setIsApiReady(false);
|
||||
|
||||
if (__DEV__) console.log('[Pezkuwi] Network switched successfully');
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('[Pezkuwi] Failed to switch network:', err);
|
||||
setError('Failed to switch network');
|
||||
}
|
||||
};
|
||||
|
||||
// Update selected account storage when it changes
|
||||
useEffect(() => {
|
||||
if (selectedAccount) {
|
||||
AsyncStorage.setItem(SELECTED_ACCOUNT_KEY, selectedAccount.address);
|
||||
}
|
||||
}, [selectedAccount]);
|
||||
|
||||
const value: PezkuwiContextType = {
|
||||
api,
|
||||
isApiReady,
|
||||
isReady,
|
||||
accounts,
|
||||
selectedAccount,
|
||||
setSelectedAccount,
|
||||
currentNetwork,
|
||||
switchNetwork,
|
||||
connectWallet,
|
||||
disconnectWallet,
|
||||
createWallet,
|
||||
importWallet,
|
||||
getKeyPair,
|
||||
signMessage,
|
||||
error,
|
||||
};
|
||||
|
||||
return <PezkuwiContext.Provider value={value}>{children}</PezkuwiContext.Provider>;
|
||||
};
|
||||
|
||||
// Hook to use Pezkuwi context
|
||||
export const usePezkuwi = (): PezkuwiContextType => {
|
||||
const context = useContext(PezkuwiContext);
|
||||
if (!context) {
|
||||
throw new Error('usePezkuwi must be used within PezkuwiProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,269 +0,0 @@
|
||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { ApiPromise, WsProvider } from '@pezkuwi/api';
|
||||
import { Keyring } from '@pezkuwi/keyring';
|
||||
import { KeyringPair } from '@pezkuwi/keyring/types';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { cryptoWaitReady } from '@pezkuwi/util-crypto';
|
||||
import { DEFAULT_ENDPOINT } from '../../../shared/blockchain/polkadot';
|
||||
|
||||
interface Account {
|
||||
address: string;
|
||||
name: string;
|
||||
meta?: {
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PolkadotContextType {
|
||||
api: ApiPromise | null;
|
||||
isApiReady: boolean;
|
||||
isConnected: boolean;
|
||||
accounts: Account[];
|
||||
selectedAccount: Account | null;
|
||||
setSelectedAccount: (account: Account | null) => void;
|
||||
connectWallet: () => Promise<void>;
|
||||
disconnectWallet: () => void;
|
||||
createWallet: (name: string, mnemonic?: string) => Promise<{ address: string; mnemonic: string }>;
|
||||
getKeyPair: (address: string) => Promise<KeyringPair | null>;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const PolkadotContext = createContext<PolkadotContextType | undefined>(undefined);
|
||||
|
||||
const WALLET_STORAGE_KEY = '@pezkuwi_wallets';
|
||||
const SELECTED_ACCOUNT_KEY = '@pezkuwi_selected_account';
|
||||
|
||||
interface PolkadotProviderProps {
|
||||
children: ReactNode;
|
||||
endpoint?: string;
|
||||
}
|
||||
|
||||
export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
||||
children,
|
||||
endpoint = DEFAULT_ENDPOINT, // Beta testnet RPC from shared config
|
||||
}) => {
|
||||
const [api, setApi] = useState<ApiPromise | null>(null);
|
||||
const [isApiReady, setIsApiReady] = useState(false);
|
||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [keyring, setKeyring] = useState<Keyring | null>(null);
|
||||
|
||||
// Initialize crypto and keyring
|
||||
useEffect(() => {
|
||||
const initCrypto = async () => {
|
||||
try {
|
||||
await cryptoWaitReady();
|
||||
const kr = new Keyring({ type: 'sr25519' });
|
||||
setKeyring(kr);
|
||||
if (__DEV__) console.warn('✅ Crypto libraries initialized');
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('❌ Failed to initialize crypto:', err);
|
||||
setError('Failed to initialize crypto libraries');
|
||||
}
|
||||
};
|
||||
|
||||
initCrypto();
|
||||
}, []);
|
||||
|
||||
// Initialize Polkadot API
|
||||
useEffect(() => {
|
||||
const initApi = async () => {
|
||||
try {
|
||||
if (__DEV__) console.warn('🔗 Connecting to Pezkuwi node:', endpoint);
|
||||
|
||||
const provider = new WsProvider(endpoint);
|
||||
const apiInstance = await ApiPromise.create({ provider });
|
||||
|
||||
await apiInstance.isReady;
|
||||
|
||||
setApi(apiInstance);
|
||||
setIsApiReady(true);
|
||||
setError(null);
|
||||
|
||||
if (__DEV__) console.warn('✅ Connected to Pezkuwi node');
|
||||
|
||||
// Get chain info
|
||||
const [chain, nodeName, nodeVersion] = await Promise.all([
|
||||
apiInstance.rpc.system.chain(),
|
||||
apiInstance.rpc.system.name(),
|
||||
apiInstance.rpc.system.version(),
|
||||
]);
|
||||
|
||||
if (__DEV__) {
|
||||
console.warn(`📡 Chain: ${chain}`);
|
||||
console.warn(`🖥️ Node: ${nodeName} v${nodeVersion}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('❌ Failed to connect to node:', err);
|
||||
setError(`Failed to connect to node: ${endpoint}`);
|
||||
setIsApiReady(false);
|
||||
}
|
||||
};
|
||||
|
||||
initApi();
|
||||
|
||||
return () => {
|
||||
if (api) {
|
||||
api.disconnect();
|
||||
}
|
||||
};
|
||||
}, [endpoint, api]);
|
||||
|
||||
// Load stored accounts on mount
|
||||
useEffect(() => {
|
||||
const loadAccounts = async () => {
|
||||
try {
|
||||
const stored = await AsyncStorage.getItem(WALLET_STORAGE_KEY);
|
||||
if (stored) {
|
||||
const wallets = JSON.parse(stored);
|
||||
setAccounts(wallets);
|
||||
|
||||
// Load selected account
|
||||
const selectedAddr = await AsyncStorage.getItem(SELECTED_ACCOUNT_KEY);
|
||||
if (selectedAddr) {
|
||||
const account = wallets.find((w: Account) => w.address === selectedAddr);
|
||||
if (account) {
|
||||
setSelectedAccount(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('Failed to load accounts:', err);
|
||||
}
|
||||
};
|
||||
|
||||
loadAccounts();
|
||||
}, []);
|
||||
|
||||
// Create a new wallet
|
||||
const createWallet = async (
|
||||
name: string,
|
||||
mnemonic?: string
|
||||
): Promise<{ address: string; mnemonic: string }> => {
|
||||
if (!keyring) {
|
||||
throw new Error('Keyring not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate or use provided mnemonic
|
||||
const mnemonicPhrase = mnemonic || Keyring.prototype.generateMnemonic();
|
||||
|
||||
// Create account from mnemonic
|
||||
const pair = keyring.addFromMnemonic(mnemonicPhrase, { name });
|
||||
|
||||
const newAccount: Account = {
|
||||
address: pair.address,
|
||||
name,
|
||||
meta: { name },
|
||||
};
|
||||
|
||||
// Store account (address only, not the seed!)
|
||||
const updatedAccounts = [...accounts, newAccount];
|
||||
setAccounts(updatedAccounts);
|
||||
await AsyncStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(updatedAccounts));
|
||||
|
||||
// SECURITY: Store encrypted seed in SecureStore (encrypted hardware-backed storage)
|
||||
const seedKey = `pezkuwi_seed_${pair.address}`;
|
||||
await SecureStore.setItemAsync(seedKey, mnemonicPhrase);
|
||||
|
||||
if (__DEV__) console.warn('✅ Wallet created:', pair.address);
|
||||
|
||||
return {
|
||||
address: pair.address,
|
||||
mnemonic: mnemonicPhrase,
|
||||
};
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('❌ Failed to create wallet:', err);
|
||||
throw new Error('Failed to create wallet');
|
||||
}
|
||||
};
|
||||
|
||||
// Get keypair for signing transactions
|
||||
const getKeyPair = async (address: string): Promise<KeyringPair | null> => {
|
||||
if (!keyring) {
|
||||
throw new Error('Keyring not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// SECURITY: Load seed from SecureStore (encrypted storage)
|
||||
const seedKey = `pezkuwi_seed_${address}`;
|
||||
const mnemonic = await SecureStore.getItemAsync(seedKey);
|
||||
|
||||
if (!mnemonic) {
|
||||
if (__DEV__) console.error('No seed found for address:', address);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Recreate keypair from mnemonic
|
||||
const pair = keyring.addFromMnemonic(mnemonic);
|
||||
return pair;
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('Failed to get keypair:', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Connect wallet (load existing accounts)
|
||||
const connectWallet = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
if (accounts.length === 0) {
|
||||
setError('No wallets found. Please create a wallet first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-select first account if none selected
|
||||
if (!selectedAccount && accounts.length > 0) {
|
||||
setSelectedAccount(accounts[0]);
|
||||
await AsyncStorage.setItem(SELECTED_ACCOUNT_KEY, accounts[0].address);
|
||||
}
|
||||
|
||||
if (__DEV__) console.warn(`✅ Connected with ${accounts.length} account(s)`);
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error('❌ Wallet connection failed:', err);
|
||||
setError('Failed to connect wallet');
|
||||
}
|
||||
};
|
||||
|
||||
// Disconnect wallet
|
||||
const disconnectWallet = () => {
|
||||
setSelectedAccount(null);
|
||||
AsyncStorage.removeItem(SELECTED_ACCOUNT_KEY);
|
||||
if (__DEV__) console.warn('🔌 Wallet disconnected');
|
||||
};
|
||||
|
||||
// Update selected account storage when it changes
|
||||
useEffect(() => {
|
||||
if (selectedAccount) {
|
||||
AsyncStorage.setItem(SELECTED_ACCOUNT_KEY, selectedAccount.address);
|
||||
}
|
||||
}, [selectedAccount]);
|
||||
|
||||
const value: PolkadotContextType = {
|
||||
api,
|
||||
isApiReady,
|
||||
isConnected: isApiReady,
|
||||
accounts,
|
||||
selectedAccount,
|
||||
setSelectedAccount,
|
||||
connectWallet,
|
||||
disconnectWallet,
|
||||
createWallet,
|
||||
getKeyPair,
|
||||
error,
|
||||
};
|
||||
|
||||
return <PolkadotContext.Provider value={value}>{children}</PolkadotContext.Provider>;
|
||||
};
|
||||
|
||||
// Hook to use Polkadot context
|
||||
export const usePolkadot = (): PolkadotContextType => {
|
||||
const context = useContext(PolkadotContext);
|
||||
if (!context) {
|
||||
throw new Error('usePolkadot must be used within PolkadotProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
+16
-16
@@ -1,20 +1,20 @@
|
||||
import React from 'react';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react-native';
|
||||
import { PolkadotProvider, usePolkadot } from '../PolkadotContext';
|
||||
import { PezkuwiProvider, usePezkuwi } from './PezkuwiContext';
|
||||
import { ApiPromise } from '@pezkuwi/api';
|
||||
|
||||
// Wrapper for provider
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PolkadotProvider>{children}</PolkadotProvider>
|
||||
<PezkuwiProvider>{children}</PezkuwiProvider>
|
||||
);
|
||||
|
||||
describe('PolkadotContext', () => {
|
||||
describe('PezkuwiContext', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should provide polkadot context', () => {
|
||||
const { result } = renderHook(() => usePolkadot(), { wrapper });
|
||||
it('should provide pezkuwi context', () => {
|
||||
const { result } = renderHook(() => usePezkuwi(), { wrapper });
|
||||
|
||||
expect(result.current).toBeDefined();
|
||||
expect(result.current.api).toBeNull();
|
||||
@@ -23,7 +23,7 @@ describe('PolkadotContext', () => {
|
||||
});
|
||||
|
||||
it('should initialize API connection', async () => {
|
||||
const { result } = renderHook(() => usePolkadot(), { wrapper });
|
||||
const { result } = renderHook(() => usePezkuwi(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isApiReady).toBe(false); // Mock doesn't complete
|
||||
@@ -31,14 +31,14 @@ describe('PolkadotContext', () => {
|
||||
});
|
||||
|
||||
it('should provide connectWallet function', () => {
|
||||
const { result } = renderHook(() => usePolkadot(), { wrapper });
|
||||
const { result } = renderHook(() => usePezkuwi(), { wrapper });
|
||||
|
||||
expect(result.current.connectWallet).toBeDefined();
|
||||
expect(typeof result.current.connectWallet).toBe('function');
|
||||
});
|
||||
|
||||
it('should handle disconnectWallet', () => {
|
||||
const { result } = renderHook(() => usePolkadot(), { wrapper });
|
||||
const { result } = renderHook(() => usePezkuwi(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.disconnectWallet();
|
||||
@@ -48,14 +48,14 @@ describe('PolkadotContext', () => {
|
||||
});
|
||||
|
||||
it('should provide setSelectedAccount function', () => {
|
||||
const { result } = renderHook(() => usePolkadot(), { wrapper });
|
||||
const { result } = renderHook(() => usePezkuwi(), { wrapper });
|
||||
|
||||
expect(result.current.setSelectedAccount).toBeDefined();
|
||||
expect(typeof result.current.setSelectedAccount).toBe('function');
|
||||
});
|
||||
|
||||
it('should set selected account', () => {
|
||||
const { result } = renderHook(() => usePolkadot(), { wrapper });
|
||||
const { result } = renderHook(() => usePezkuwi(), { wrapper });
|
||||
|
||||
const testAccount = { address: '5test', name: 'Test Account' };
|
||||
|
||||
@@ -67,31 +67,31 @@ describe('PolkadotContext', () => {
|
||||
});
|
||||
|
||||
it('should provide getKeyPair function', () => {
|
||||
const { result } = renderHook(() => usePolkadot(), { wrapper });
|
||||
const { result } = renderHook(() => usePezkuwi(), { wrapper });
|
||||
|
||||
expect(result.current.getKeyPair).toBeDefined();
|
||||
expect(typeof result.current.getKeyPair).toBe('function');
|
||||
});
|
||||
|
||||
it('should throw error when usePolkadot is used outside provider', () => {
|
||||
it('should throw error when usePezkuwi is used outside provider', () => {
|
||||
// Suppress console error for this test
|
||||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => usePolkadot());
|
||||
}).toThrow('usePolkadot must be used within PolkadotProvider');
|
||||
renderHook(() => usePezkuwi());
|
||||
}).toThrow('usePezkuwi must be used within PezkuwiProvider');
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle accounts array', () => {
|
||||
const { result } = renderHook(() => usePolkadot(), { wrapper });
|
||||
const { result } = renderHook(() => usePezkuwi(), { wrapper });
|
||||
|
||||
expect(Array.isArray(result.current.accounts)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle error state', () => {
|
||||
const { result } = renderHook(() => usePolkadot(), { wrapper });
|
||||
const { result } = renderHook(() => usePezkuwi(), { wrapper });
|
||||
|
||||
expect(result.current.error).toBeDefined();
|
||||
});
|
||||
Reference in New Issue
Block a user