diff --git a/mobile/src/theme/colors.ts b/mobile/src/theme/colors.ts index 1241d92f..53a9bd96 100644 --- a/mobile/src/theme/colors.ts +++ b/mobile/src/theme/colors.ts @@ -1,25 +1,6 @@ -// Kurdistan Flag Colors -export const KurdistanColors = { - kesk: '#00A94F', // Green - Primary - sor: '#EE2A35', // Red - Accent - zer: '#FFD700', // Gold - Secondary - spi: '#FFFFFF', // White - Background - reş: '#000000', // Black - Text -}; - -export const AppColors = { - primary: KurdistanColors.kesk, - secondary: KurdistanColors.zer, - accent: KurdistanColors.sor, - background: '#F5F5F5', - surface: KurdistanColors.spi, - text: KurdistanColors.reş, - textSecondary: '#666666', - border: '#E0E0E0', - error: KurdistanColors.sor, - success: KurdistanColors.kesk, - warning: KurdistanColors.zer, - info: '#2196F3', -}; - -export default AppColors; +/** + * Re-export colors from shared theme + * All color definitions are centralized in shared/theme/colors.ts + */ +export { KurdistanColors, AppColors } from '../../../shared/theme/colors'; +export { AppColors as default } from '../../../shared/theme/colors'; diff --git a/mobile/tsconfig.json b/mobile/tsconfig.json index b9567f60..29c8138d 100644 --- a/mobile/tsconfig.json +++ b/mobile/tsconfig.json @@ -1,6 +1,14 @@ { "extends": "expo/tsconfig.base", "compilerOptions": { - "strict": true + "strict": true, + "baseUrl": ".", + "paths": { + "@pezkuwi/lib/*": ["../shared/lib/*"], + "@pezkuwi/utils/*": ["../shared/utils/*"], + "@pezkuwi/theme/*": ["../shared/theme/*"], + "@pezkuwi/types/*": ["../shared/types/*"], + "@pezkuwi/i18n": ["../shared/i18n"] + } } } diff --git a/web/src/lib/citizenship-workflow.ts b/shared/lib/citizenship-workflow.ts similarity index 100% rename from web/src/lib/citizenship-workflow.ts rename to shared/lib/citizenship-workflow.ts diff --git a/web/src/lib/identity.ts b/shared/lib/identity.ts similarity index 100% rename from web/src/lib/identity.ts rename to shared/lib/identity.ts diff --git a/web/src/lib/multisig.ts b/shared/lib/multisig.ts similarity index 100% rename from web/src/lib/multisig.ts rename to shared/lib/multisig.ts diff --git a/web/src/lib/scores.ts b/shared/lib/scores.ts similarity index 100% rename from web/src/lib/scores.ts rename to shared/lib/scores.ts diff --git a/web/src/lib/staking.ts b/shared/lib/staking.ts similarity index 100% rename from web/src/lib/staking.ts rename to shared/lib/staking.ts diff --git a/web/src/lib/tiki.ts b/shared/lib/tiki.ts similarity index 100% rename from web/src/lib/tiki.ts rename to shared/lib/tiki.ts diff --git a/web/src/lib/usdt.ts b/shared/lib/usdt.ts similarity index 100% rename from web/src/lib/usdt.ts rename to shared/lib/usdt.ts diff --git a/web/src/lib/wallet.ts b/shared/lib/wallet.ts similarity index 100% rename from web/src/lib/wallet.ts rename to shared/lib/wallet.ts diff --git a/shared/theme/colors.ts b/shared/theme/colors.ts new file mode 100644 index 00000000..5ea53ef9 --- /dev/null +++ b/shared/theme/colors.ts @@ -0,0 +1,30 @@ +/** + * Shared theme colors for all platforms + */ + +// Kurdistan Flag Colors +export const KurdistanColors = { + kesk: '#00A94F', // Green - Primary + sor: '#EE2A35', // Red - Accent + zer: '#FFD700', // Gold - Secondary + spi: '#FFFFFF', // White - Background + reş: '#000000', // Black - Text +}; + +// Application color palette +export const AppColors = { + primary: KurdistanColors.kesk, + secondary: KurdistanColors.zer, + accent: KurdistanColors.sor, + background: '#F5F5F5', + surface: KurdistanColors.spi, + text: KurdistanColors.reş, + textSecondary: '#666666', + border: '#E0E0E0', + error: KurdistanColors.sor, + success: KurdistanColors.kesk, + warning: KurdistanColors.zer, + info: '#2196F3', +}; + +export default AppColors; diff --git a/web/src/utils/auth.ts b/shared/utils/auth.ts similarity index 100% rename from web/src/utils/auth.ts rename to shared/utils/auth.ts diff --git a/web/src/utils/dex.ts b/shared/utils/dex.ts similarity index 100% rename from web/src/utils/dex.ts rename to shared/utils/dex.ts diff --git a/shared/utils/format.ts b/shared/utils/format.ts new file mode 100644 index 00000000..cc47ad61 --- /dev/null +++ b/shared/utils/format.ts @@ -0,0 +1,74 @@ +/** + * Shared formatting utilities + * Platform-agnostic formatters for numbers, currency, etc. + */ + +/** + * Format a number with K, M, B suffixes for large values + * @param value - The number to format + * @param decimals - Number of decimal places (default 2) + * @returns Formatted string + */ +export function formatNumber(value: number, decimals: number = 2): string { + if (value === 0) return '0'; + if (value < 0.01) return '<0.01'; + + // For large numbers, use K, M, B suffixes + if (value >= 1e9) { + return (value / 1e9).toFixed(decimals) + 'B'; + } + if (value >= 1e6) { + return (value / 1e6).toFixed(decimals) + 'M'; + } + if (value >= 1e3) { + return (value / 1e3).toFixed(decimals) + 'K'; + } + + return value.toFixed(decimals); +} + +/** + * Format a percentage value + * @param value - The percentage value (e.g., 0.15 for 15%) + * @param decimals - Number of decimal places (default 2) + * @returns Formatted percentage string + */ +export function formatPercentage(value: number, decimals: number = 2): string { + return `${(value * 100).toFixed(decimals)}%`; +} + +/** + * Format a currency value + * @param value - The currency value + * @param symbol - Currency symbol (default '$') + * @param decimals - Number of decimal places (default 2) + * @returns Formatted currency string + */ +export function formatCurrency( + value: number, + symbol: string = '$', + decimals: number = 2 +): string { + return `${symbol}${formatNumber(value, decimals)}`; +} + +/** + * Parse a formatted number string back to number + * @param formatted - Formatted string (e.g., "1.5K") + * @returns Number value + */ +export function parseFormattedNumber(formatted: string): number { + const cleaned = formatted.replace(/[^0-9.KMB]/g, ''); + const match = cleaned.match(/^([\d.]+)([KMB])?$/); + + if (!match) return 0; + + const [, numberPart, suffix] = match; + let value = parseFloat(numberPart); + + if (suffix === 'K') value *= 1e3; + else if (suffix === 'M') value *= 1e6; + else if (suffix === 'B') value *= 1e9; + + return value; +} diff --git a/web/src/components/AccountBalance.tsx b/web/src/components/AccountBalance.tsx index bc529cd5..69b2a00b 100644 --- a/web/src/components/AccountBalance.tsx +++ b/web/src/components/AccountBalance.tsx @@ -3,10 +3,10 @@ import { usePolkadot } from '@/contexts/PolkadotContext'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Wallet, TrendingUp, ArrowUpRight, ArrowDownRight, RefreshCw, Award, Plus, Coins, Send, Shield, Users } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet'; +import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet'; import { AddTokenModal } from './AddTokenModal'; import { TransferModal } from './TransferModal'; -import { getAllScores, type UserScores } from '@/lib/scores'; +import { getAllScores, type UserScores } from '@pezkuwi/lib/scores'; interface TokenBalance { assetId: number; diff --git a/web/src/components/AddLiquidityModal.tsx b/web/src/components/AddLiquidityModal.tsx index 341ec616..c1b68177 100644 --- a/web/src/components/AddLiquidityModal.tsx +++ b/web/src/components/AddLiquidityModal.tsx @@ -5,7 +5,7 @@ import { usePolkadot } from '@/contexts/PolkadotContext'; import { useWallet } from '@/contexts/WalletContext'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet'; +import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet'; interface AddLiquidityModalProps { isOpen: boolean; diff --git a/web/src/components/HeroSection.tsx b/web/src/components/HeroSection.tsx index 5cee9a12..05346962 100644 --- a/web/src/components/HeroSection.tsx +++ b/web/src/components/HeroSection.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { ChevronRight, Shield } from 'lucide-react'; import { usePolkadot } from '../contexts/PolkadotContext'; -import { formatBalance } from '../lib/wallet'; +import { formatBalance } from '@pezkuwi/lib/wallet'; const HeroSection: React.FC = () => { const { t } = useTranslation(); diff --git a/web/src/components/MultisigMembers.tsx b/web/src/components/MultisigMembers.tsx index 4c7b9566..30b1e545 100644 --- a/web/src/components/MultisigMembers.tsx +++ b/web/src/components/MultisigMembers.tsx @@ -9,8 +9,8 @@ import { calculateMultisigAddress, USDT_MULTISIG_CONFIG, formatMultisigAddress, -} from '@/lib/multisig'; -import { getTikiDisplayName, getTikiEmoji } from '@/lib/tiki'; +} from '@pezkuwi/lib/multisig'; +import { getTikiDisplayName, getTikiEmoji } from '@pezkuwi/lib/tiki'; interface MultisigMembersProps { specificAddresses?: Record; diff --git a/web/src/components/NftList.tsx b/web/src/components/NftList.tsx index 9f539793..c681f560 100644 --- a/web/src/components/NftList.tsx +++ b/web/src/components/NftList.tsx @@ -3,8 +3,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge'; import { Loader2, Award, Crown, Shield, Users } from 'lucide-react'; import { usePolkadot } from '@/contexts/PolkadotContext'; -import { getUserTikis } from '@/lib/citizenship-workflow'; -import type { TikiInfo } from '@/lib/citizenship-workflow'; +import { getUserTikis } from '@pezkuwi/lib/citizenship-workflow'; +import type { TikiInfo } from '@pezkuwi/lib/citizenship-workflow'; // Icon map for different Tiki roles const getTikiIcon = (role: string) => { diff --git a/web/src/components/PoolDashboard.tsx b/web/src/components/PoolDashboard.tsx index 98c463d8..cbb90dae 100644 --- a/web/src/components/PoolDashboard.tsx +++ b/web/src/components/PoolDashboard.tsx @@ -8,7 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { usePolkadot } from '@/contexts/PolkadotContext'; import { useWallet } from '@/contexts/WalletContext'; -import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet'; +import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet'; import { AddLiquidityModal } from '@/components/AddLiquidityModal'; import { RemoveLiquidityModal } from '@/components/RemoveLiquidityModal'; diff --git a/web/src/components/RemoveLiquidityModal.tsx b/web/src/components/RemoveLiquidityModal.tsx index 9b27ab8a..f1569437 100644 --- a/web/src/components/RemoveLiquidityModal.tsx +++ b/web/src/components/RemoveLiquidityModal.tsx @@ -5,7 +5,7 @@ import { usePolkadot } from '@/contexts/PolkadotContext'; import { useWallet } from '@/contexts/WalletContext'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet'; +import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet'; // Helper to get display name for tokens (users see HEZ not wHEZ, USDT not wUSDT) const getDisplayTokenName = (assetId: number): string => { diff --git a/web/src/components/ReservesDashboard.tsx b/web/src/components/ReservesDashboard.tsx index 5dd9e674..f4e23414 100644 --- a/web/src/components/ReservesDashboard.tsx +++ b/web/src/components/ReservesDashboard.tsx @@ -6,7 +6,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { usePolkadot } from '@/contexts/PolkadotContext'; -import { getWUSDTTotalSupply, checkReserveHealth, formatWUSDT } from '@/lib/usdt'; +import { getWUSDTTotalSupply, checkReserveHealth, formatWUSDT } from '@pezkuwi/lib/usdt'; import { MultisigMembers } from './MultisigMembers'; interface ReservesDashboardProps { diff --git a/web/src/components/TokenSwap.tsx b/web/src/components/TokenSwap.tsx index 7baf872a..46b32fac 100644 --- a/web/src/components/TokenSwap.tsx +++ b/web/src/components/TokenSwap.tsx @@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { usePolkadot } from '@/contexts/PolkadotContext'; import { useWallet } from '@/contexts/WalletContext'; -import { ASSET_IDS, formatBalance, parseAmount } from '@/lib/wallet'; +import { ASSET_IDS, formatBalance, parseAmount } from '@pezkuwi/lib/wallet'; import { useToast } from '@/hooks/use-toast'; import { KurdistanSun } from './KurdistanSun'; import { PriceChart } from './trading/PriceChart'; diff --git a/web/src/components/USDTBridge.tsx b/web/src/components/USDTBridge.tsx index 240546d7..5c1fc801 100644 --- a/web/src/components/USDTBridge.tsx +++ b/web/src/components/USDTBridge.tsx @@ -13,8 +13,8 @@ import { getWithdrawalTier, formatDelay, formatWUSDT, -} from '@/lib/usdt'; -import { isMultisigMember } from '@/lib/multisig'; +} from '@pezkuwi/lib/usdt'; +import { isMultisigMember } from '@pezkuwi/lib/multisig'; interface USDTBridgeProps { isOpen: boolean; diff --git a/web/src/components/citizenship/ExistingCitizenAuth.tsx b/web/src/components/citizenship/ExistingCitizenAuth.tsx index 445fc89e..3299b97c 100644 --- a/web/src/components/citizenship/ExistingCitizenAuth.tsx +++ b/web/src/components/citizenship/ExistingCitizenAuth.tsx @@ -6,9 +6,9 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Loader2, CheckCircle, AlertTriangle, Shield } from 'lucide-react'; import { usePolkadot } from '@/contexts/PolkadotContext'; -import { verifyNftOwnership } from '@/lib/citizenship-workflow'; -import { generateAuthChallenge, signChallenge, verifySignature, saveCitizenSession } from '@/lib/citizenship-crypto'; -import type { AuthChallenge } from '@/lib/citizenship-crypto'; +import { verifyNftOwnership } from '@pezkuwi/lib/citizenship-workflow'; +import { generateAuthChallenge, signChallenge, verifySignature, saveCitizenSession } from '@pezkuwi/lib/citizenship-crypto'; +import type { AuthChallenge } from '@pezkuwi/lib/citizenship-crypto'; interface ExistingCitizenAuthProps { onClose: () => void; diff --git a/web/src/components/citizenship/NewCitizenApplication.tsx b/web/src/components/citizenship/NewCitizenApplication.tsx index ba0c3f62..e3a724eb 100644 --- a/web/src/components/citizenship/NewCitizenApplication.tsx +++ b/web/src/components/citizenship/NewCitizenApplication.tsx @@ -10,9 +10,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Checkbox } from '@/components/ui/checkbox'; import { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Clock } from 'lucide-react'; import { usePolkadot } from '@/contexts/PolkadotContext'; -import type { CitizenshipData, Region, MaritalStatus } from '@/lib/citizenship-workflow'; -import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } from '@/lib/citizenship-workflow'; -import { generateCommitmentHash, generateNullifierHash, encryptData, saveLocalCitizenshipData, uploadToIPFS } from '@/lib/citizenship-crypto'; +import type { CitizenshipData, Region, MaritalStatus } from '@pezkuwi/lib/citizenship-workflow'; +import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } from '@pezkuwi/lib/citizenship-workflow'; +import { generateCommitmentHash, generateNullifierHash, encryptData, saveLocalCitizenshipData, uploadToIPFS } from '@pezkuwi/lib/citizenship-crypto'; interface NewCitizenApplicationProps { onClose: () => void; diff --git a/web/src/components/dex/AddLiquidityModal.tsx b/web/src/components/dex/AddLiquidityModal.tsx index 3a491d90..9876e0a6 100644 --- a/web/src/components/dex/AddLiquidityModal.tsx +++ b/web/src/components/dex/AddLiquidityModal.tsx @@ -4,7 +4,7 @@ import { useWallet } from '@/contexts/WalletContext'; import { X, Plus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { PoolInfo } from '@/types/dex'; -import { parseTokenInput, formatTokenBalance, quote } from '@/utils/dex'; +import { parseTokenInput, formatTokenBalance, quote } from '@pezkuwi/utils/dex'; interface AddLiquidityModalProps { isOpen: boolean; diff --git a/web/src/components/dex/CreatePoolModal.tsx b/web/src/components/dex/CreatePoolModal.tsx index 65b7e1be..df74a61f 100644 --- a/web/src/components/dex/CreatePoolModal.tsx +++ b/web/src/components/dex/CreatePoolModal.tsx @@ -5,7 +5,7 @@ import { X, Plus, AlertCircle, Loader2, CheckCircle } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { KNOWN_TOKENS } from '@/types/dex'; -import { parseTokenInput, formatTokenBalance } from '@/utils/dex'; +import { parseTokenInput, formatTokenBalance } from '@pezkuwi/utils/dex'; interface CreatePoolModalProps { isOpen: boolean; diff --git a/web/src/components/dex/DEXDashboard.tsx b/web/src/components/dex/DEXDashboard.tsx index 51160d5d..65e0ce07 100644 --- a/web/src/components/dex/DEXDashboard.tsx +++ b/web/src/components/dex/DEXDashboard.tsx @@ -6,7 +6,7 @@ import PoolDashboard from '@/components/PoolDashboard'; import { CreatePoolModal } from './CreatePoolModal'; import { InitializeHezPoolModal } from './InitializeHezPoolModal'; import { ArrowRightLeft, Droplet, Settings } from 'lucide-react'; -import { isFounderWallet } from '@/utils/auth'; +import { isFounderWallet } from '@pezkuwi/utils/auth'; export const DEXDashboard: React.FC = () => { const { account } = useWallet(); diff --git a/web/src/components/dex/PoolBrowser.tsx b/web/src/components/dex/PoolBrowser.tsx index abaa38ed..6e12352f 100644 --- a/web/src/components/dex/PoolBrowser.tsx +++ b/web/src/components/dex/PoolBrowser.tsx @@ -5,8 +5,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { TrendingUp, Droplet, BarChart3, Search, Plus } from 'lucide-react'; import { PoolInfo } from '@/types/dex'; -import { fetchPools, formatTokenBalance } from '@/utils/dex'; -import { isFounderWallet } from '@/utils/auth'; +import { fetchPools, formatTokenBalance } from '@pezkuwi/utils/dex'; +import { isFounderWallet } from '@pezkuwi/utils/auth'; interface PoolBrowserProps { onAddLiquidity?: (pool: PoolInfo) => void; diff --git a/web/src/components/dex/RemoveLiquidityModal.tsx b/web/src/components/dex/RemoveLiquidityModal.tsx index bc478b24..15d76e4f 100644 --- a/web/src/components/dex/RemoveLiquidityModal.tsx +++ b/web/src/components/dex/RemoveLiquidityModal.tsx @@ -4,7 +4,7 @@ import { useWallet } from '@/contexts/WalletContext'; import { X, Minus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { PoolInfo } from '@/types/dex'; -import { formatTokenBalance } from '@/utils/dex'; +import { formatTokenBalance } from '@pezkuwi/utils/dex'; interface RemoveLiquidityModalProps { isOpen: boolean; diff --git a/web/src/components/dex/SwapInterface.tsx b/web/src/components/dex/SwapInterface.tsx index a7deabce..b4ab0611 100644 --- a/web/src/components/dex/SwapInterface.tsx +++ b/web/src/components/dex/SwapInterface.tsx @@ -14,7 +14,7 @@ import { formatTokenBalance, getAmountOut, calculatePriceImpact, -} from '@/utils/dex'; +} from '@pezkuwi/utils/dex'; import { useToast } from '@/hooks/use-toast'; interface SwapInterfaceProps { diff --git a/web/src/components/governance/GovernanceOverview.tsx b/web/src/components/governance/GovernanceOverview.tsx index 8cd95183..73fd3adc 100644 --- a/web/src/components/governance/GovernanceOverview.tsx +++ b/web/src/components/governance/GovernanceOverview.tsx @@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; import { Badge } from '../ui/badge'; import { Progress } from '../ui/progress'; import { usePolkadot } from '../../contexts/PolkadotContext'; -import { formatBalance } from '../../lib/wallet'; +import { formatBalance } from '../@pezkuwi/lib/wallet'; interface GovernanceStats { activeProposals: number; diff --git a/web/src/components/staking/StakingDashboard.tsx b/web/src/components/staking/StakingDashboard.tsx index 20801d8b..c4d73169 100644 --- a/web/src/components/staking/StakingDashboard.tsx +++ b/web/src/components/staking/StakingDashboard.tsx @@ -20,7 +20,7 @@ import { getCurrentEra, parseAmount, type StakingInfo -} from '@/lib/staking'; +} from '@pezkuwi/lib/staking'; export const StakingDashboard: React.FC = () => { const { t } = useTranslation(); diff --git a/web/src/components/wallet/WalletButton.tsx b/web/src/components/wallet/WalletButton.tsx index b6fa9b02..ad0b9192 100644 --- a/web/src/components/wallet/WalletButton.tsx +++ b/web/src/components/wallet/WalletButton.tsx @@ -10,7 +10,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { useWallet } from '@/contexts/WalletContext'; -import { formatAddress, formatBalance } from '@/lib/wallet'; +import { formatAddress, formatBalance } from '@pezkuwi/lib/wallet'; import { Badge } from '@/components/ui/badge'; export const WalletButton: React.FC = () => { diff --git a/web/src/components/wallet/WalletModal.tsx b/web/src/components/wallet/WalletModal.tsx index 54f3845c..356bdab2 100644 --- a/web/src/components/wallet/WalletModal.tsx +++ b/web/src/components/wallet/WalletModal.tsx @@ -9,8 +9,8 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { usePolkadot } from '@/contexts/PolkadotContext'; -import { formatAddress } from '@/lib/wallet'; -import { getAllScores, type UserScores } from '@/lib/scores'; +import { formatAddress } from '@pezkuwi/lib/wallet'; +import { getAllScores, type UserScores } from '@pezkuwi/lib/scores'; interface WalletModalProps { isOpen: boolean; diff --git a/web/src/contexts/IdentityContext.tsx b/web/src/contexts/IdentityContext.tsx index 1a3b6fa3..6e83d4c0 100644 --- a/web/src/contexts/IdentityContext.tsx +++ b/web/src/contexts/IdentityContext.tsx @@ -9,7 +9,7 @@ import { generateZKProof, DEFAULT_BADGES, ROLES -} from '@/lib/identity'; +} from '@pezkuwi/lib/identity'; interface IdentityContextType { profile: IdentityProfile | null; diff --git a/web/src/contexts/WalletContext.tsx b/web/src/contexts/WalletContext.tsx index 328273a3..2477dba9 100644 --- a/web/src/contexts/WalletContext.tsx +++ b/web/src/contexts/WalletContext.tsx @@ -6,7 +6,7 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { usePolkadot } from './PolkadotContext'; -import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@/lib/wallet'; +import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@pezkuwi/lib/wallet'; import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; import type { Signer } from '@polkadot/api/types'; import { web3FromAddress } from '@polkadot/extension-dapp'; diff --git a/web/src/lib/citizenship-crypto.ts b/web/src/lib/citizenship-crypto.ts deleted file mode 100644 index d081acb3..00000000 --- a/web/src/lib/citizenship-crypto.ts +++ /dev/null @@ -1,404 +0,0 @@ -// ======================================== -// Citizenship Crypto Utilities -// ======================================== -// Handles encryption, hashing, signatures for citizenship data - -import { web3FromAddress } from '@polkadot/extension-dapp'; -import { stringToHex, hexToU8a, u8aToHex, stringToU8a } from '@polkadot/util'; -import { decodeAddress, signatureVerify, cryptoWaitReady } from '@polkadot/util-crypto'; -import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; -import type { CitizenshipData } from './citizenship-workflow'; - -// ======================================== -// HASHING FUNCTIONS -// ======================================== - -/** - * Generate SHA-256 hash from data - */ -export async function generateHash(data: string): Promise { - const encoder = new TextEncoder(); - const dataBuffer = encoder.encode(data); - const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - return `0x${hashHex}`; -} - -/** - * Generate commitment hash from citizenship data - */ -export async function generateCommitmentHash( - data: CitizenshipData -): Promise { - const dataString = JSON.stringify({ - fullName: data.fullName, - fatherName: data.fatherName, - grandfatherName: data.grandfatherName, - motherName: data.motherName, - tribe: data.tribe, - maritalStatus: data.maritalStatus, - childrenCount: data.childrenCount, - children: data.children, - region: data.region, - email: data.email, - profession: data.profession, - referralCode: data.referralCode, - walletAddress: data.walletAddress, - timestamp: data.timestamp - }); - - return generateHash(dataString); -} - -/** - * Generate nullifier hash (prevents double-registration) - */ -export async function generateNullifierHash( - walletAddress: string, - timestamp: number -): Promise { - const nullifierData = `${walletAddress}-${timestamp}-nullifier`; - return generateHash(nullifierData); -} - -// ======================================== -// ENCRYPTION / DECRYPTION (AES-GCM) -// ======================================== - -/** - * Derive encryption key from wallet address - * NOTE: For MVP, we use a deterministic key. For production, use proper key derivation - */ -async function deriveEncryptionKey(walletAddress: string): Promise { - // Create a deterministic seed from wallet address - const seed = await generateHash(walletAddress); - - // Convert hex to ArrayBuffer - const keyMaterial = hexToU8a(seed).slice(0, 32); // 256-bit key - - // Import as AES-GCM key - return crypto.subtle.importKey( - 'raw', - keyMaterial, - { name: 'AES-GCM', length: 256 }, - false, - ['encrypt', 'decrypt'] - ); -} - -/** - * Encrypt citizenship data - */ -export async function encryptData( - data: CitizenshipData, - walletAddress: string -): Promise { - try { - const key = await deriveEncryptionKey(walletAddress); - - // Generate random IV (Initialization Vector) - const iv = crypto.getRandomValues(new Uint8Array(12)); - - // Encrypt data - const encoder = new TextEncoder(); - const dataBuffer = encoder.encode(JSON.stringify(data)); - - const encryptedBuffer = await crypto.subtle.encrypt( - { name: 'AES-GCM', iv }, - key, - dataBuffer - ); - - // Combine IV + encrypted data - const combined = new Uint8Array(iv.length + encryptedBuffer.byteLength); - combined.set(iv, 0); - combined.set(new Uint8Array(encryptedBuffer), iv.length); - - // Convert to hex - return u8aToHex(combined); - } catch (error) { - console.error('Encryption error:', error); - throw new Error('Failed to encrypt data'); - } -} - -/** - * Decrypt citizenship data - */ -export async function decryptData( - encryptedHex: string, - walletAddress: string -): Promise { - try { - const key = await deriveEncryptionKey(walletAddress); - - // Convert hex to Uint8Array - const combined = hexToU8a(encryptedHex); - - // Extract IV and encrypted data - const iv = combined.slice(0, 12); - const encryptedData = combined.slice(12); - - // Decrypt - const decryptedBuffer = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv }, - key, - encryptedData - ); - - // Convert to string and parse JSON - const decoder = new TextDecoder(); - const decryptedString = decoder.decode(decryptedBuffer); - - return JSON.parse(decryptedString) as CitizenshipData; - } catch (error) { - console.error('Decryption error:', error); - throw new Error('Failed to decrypt data'); - } -} - -// ======================================== -// SIGNATURE GENERATION & VERIFICATION -// ======================================== - -export interface AuthChallenge { - nonce: string; // Random UUID - timestamp: number; // Current timestamp - tikiNumber: string; // NFT number to prove - expiresAt: number; // Expiry timestamp (5 min) -} - -/** - * Generate authentication challenge - */ -export function generateAuthChallenge(tikiNumber: string): AuthChallenge { - const now = Date.now(); - const nonce = crypto.randomUUID(); - - return { - nonce, - timestamp: now, - tikiNumber, - expiresAt: now + (5 * 60 * 1000) // 5 minutes - }; -} - -/** - * Format challenge message for signing - */ -export function formatChallengeMessage(challenge: AuthChallenge): string { - return `Prove ownership of Welati Tiki #${challenge.tikiNumber} - -Nonce: ${challenge.nonce} -Timestamp: ${challenge.timestamp} -Expires: ${new Date(challenge.expiresAt).toISOString()} - -By signing this message, you prove you control the wallet that owns this Tiki NFT.`; -} - -/** - * Sign authentication challenge with wallet - */ -export async function signChallenge( - account: InjectedAccountWithMeta, - challenge: AuthChallenge -): Promise { - try { - await cryptoWaitReady(); - - const injector = await web3FromAddress(account.address); - const signRaw = injector?.signer?.signRaw; - - if (!signRaw) { - throw new Error('Signer not available'); - } - - const message = formatChallengeMessage(challenge); - - const { signature } = await signRaw({ - address: account.address, - data: stringToHex(message), - type: 'bytes' - }); - - return signature; - } catch (error) { - console.error('Signature error:', error); - throw new Error('Failed to sign challenge'); - } -} - -/** - * Verify signature - */ -export async function verifySignature( - signature: string, - challenge: AuthChallenge, - expectedAddress: string -): Promise { - try { - await cryptoWaitReady(); - - // Check if challenge has expired - if (Date.now() > challenge.expiresAt) { - console.warn('Challenge has expired'); - return false; - } - - const message = formatChallengeMessage(challenge); - const messageU8a = stringToU8a(message); - const signatureU8a = hexToU8a(signature); - const publicKey = decodeAddress(expectedAddress); - - const result = signatureVerify(messageU8a, signatureU8a, publicKey); - - return result.isValid; - } catch (error) { - console.error('Verification error:', error); - return false; - } -} - -// ======================================== -// LOCAL STORAGE UTILITIES -// ======================================== - -const STORAGE_KEY_PREFIX = 'pezkuwi_citizen_'; - -export interface CitizenSession { - tikiNumber: string; - walletAddress: string; - sessionToken: string; // JWT-like token - encryptedDataCID?: string; // IPFS CID - lastAuthenticated: number; // Timestamp - expiresAt: number; // Session expiry (24h) -} - -/** - * Save encrypted citizen session to localStorage - */ -export async function saveCitizenSession(session: CitizenSession): Promise { - try { - const sessionJson = JSON.stringify(session); - const sessionKey = `${STORAGE_KEY_PREFIX}session`; - - // For MVP, store plainly. For production, encrypt with device key - localStorage.setItem(sessionKey, sessionJson); - } catch (error) { - console.error('Error saving session:', error); - throw new Error('Failed to save session'); - } -} - -/** - * Load citizen session from localStorage - */ -export function loadCitizenSession(): CitizenSession | null { - try { - const sessionKey = `${STORAGE_KEY_PREFIX}session`; - const sessionJson = localStorage.getItem(sessionKey); - - if (!sessionJson) { - return null; - } - - const session = JSON.parse(sessionJson) as CitizenSession; - - // Check if session has expired - if (Date.now() > session.expiresAt) { - clearCitizenSession(); - return null; - } - - return session; - } catch (error) { - console.error('Error loading session:', error); - return null; - } -} - -/** - * Clear citizen session from localStorage - */ -export function clearCitizenSession(): void { - try { - const sessionKey = `${STORAGE_KEY_PREFIX}session`; - localStorage.removeItem(sessionKey); - } catch (error) { - console.error('Error clearing session:', error); - } -} - -/** - * Save encrypted citizenship data to localStorage (backup) - */ -export async function saveLocalCitizenshipData( - data: CitizenshipData, - walletAddress: string -): Promise { - try { - const encrypted = await encryptData(data, walletAddress); - const dataKey = `${STORAGE_KEY_PREFIX}data_${walletAddress}`; - - localStorage.setItem(dataKey, encrypted); - } catch (error) { - console.error('Error saving citizenship data:', error); - throw new Error('Failed to save citizenship data'); - } -} - -/** - * Load encrypted citizenship data from localStorage - */ -export async function loadLocalCitizenshipData( - walletAddress: string -): Promise { - try { - const dataKey = `${STORAGE_KEY_PREFIX}data_${walletAddress}`; - const encrypted = localStorage.getItem(dataKey); - - if (!encrypted) { - return null; - } - - return await decryptData(encrypted, walletAddress); - } catch (error) { - console.error('Error loading citizenship data:', error); - return null; - } -} - -// ======================================== -// IPFS UTILITIES (Placeholder) -// ======================================== - -/** - * Upload encrypted data to IPFS - * NOTE: This is a placeholder. Implement with actual IPFS client (Pinata, Web3.Storage, etc.) - */ -export async function uploadToIPFS(encryptedData: string): Promise { - // TODO: Implement actual IPFS upload - // For MVP, we can use Pinata API or Web3.Storage - - console.warn('IPFS upload not yet implemented. Using mock CID.'); - - // Mock CID for development - const mockCid = `Qm${Math.random().toString(36).substring(2, 15)}`; - - return mockCid; -} - -/** - * Fetch encrypted data from IPFS - * NOTE: This is a placeholder. Implement with actual IPFS client - */ -export async function fetchFromIPFS(cid: string): Promise { - // TODO: Implement actual IPFS fetch - // For MVP, use public IPFS gateways or dedicated service - - console.warn('IPFS fetch not yet implemented. Returning mock data.'); - - // Mock encrypted data - return '0x000000000000000000000000'; -} diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index ac1aa290..7edb33b9 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -1,24 +1,14 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" +/** + * Web-specific className utility (uses Tailwind merge) + */ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } -export function formatNumber(value: number, decimals: number = 2): string { - if (value === 0) return '0'; - if (value < 0.01) return '<0.01'; - - // For large numbers, use K, M, B suffixes - if (value >= 1e9) { - return (value / 1e9).toFixed(decimals) + 'B'; - } - if (value >= 1e6) { - return (value / 1e6).toFixed(decimals) + 'M'; - } - if (value >= 1e3) { - return (value / 1e3).toFixed(decimals) + 'K'; - } - - return value.toFixed(decimals); -} +/** + * Re-export formatNumber from shared utils + */ +export { formatNumber } from '@pezkuwi/utils/format'; diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index 3641496e..a39527e2 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -9,8 +9,8 @@ import { usePolkadot } from '@/contexts/PolkadotContext'; import { supabase } from '@/lib/supabase'; import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft, Award, Users, TrendingUp } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; -import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories } from '@/lib/tiki'; -import { getAllScores, type UserScores } from '@/lib/scores'; +import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories } from '@pezkuwi/lib/tiki'; +import { getAllScores, type UserScores } from '@pezkuwi/lib/scores'; export default function Dashboard() { const { user } = useAuth(); diff --git a/web/tsconfig.json b/web/tsconfig.json index 7a6b491f..ab774130 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -8,7 +8,11 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], - "@pezkuwi/i18n": ["../shared/i18n"] + "@pezkuwi/i18n": ["../shared/i18n"], + "@pezkuwi/lib": ["../shared/lib"], + "@pezkuwi/utils": ["../shared/utils"], + "@pezkuwi/theme": ["../shared/theme"], + "@pezkuwi/types": ["../shared/types"] }, "noImplicitAny": false, "noUnusedParameters": false, diff --git a/web/vite.config.ts b/web/vite.config.ts index 4bbc7bfa..4ceb9a59 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -23,6 +23,11 @@ export default defineConfig(({ mode }) => ({ alias: { "@": path.resolve(__dirname, "./src"), "@pezkuwi/i18n": path.resolve(__dirname, "../shared/i18n"), + "@pezkuwi/lib": path.resolve(__dirname, "../shared/lib"), + "@pezkuwi/utils": path.resolve(__dirname, "../shared/utils"), + "@pezkuwi/theme": path.resolve(__dirname, "../shared/theme"), + "@pezkuwi/types": path.resolve(__dirname, "../shared/types"), + 'buffer': 'buffer/', }, }, json: {