mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 11:18:01 +00:00
Fix all shadow deprecation warnings across entire mobile app
- Replaced shadowColor/shadowOffset/shadowOpacity/shadowRadius with boxShadow - Fixed 28 files (21 screens + 7 components) - Preserved elevation for Android compatibility - All React Native Web deprecation warnings resolved Files fixed: - All screen components - All reusable components - Navigation components - Modal components
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
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',
|
||||
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;
|
||||
@@ -0,0 +1,515 @@
|
||||
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;
|
||||
@@ -66,10 +66,7 @@ const styles = StyleSheet.create({
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
elevation: 3,
|
||||
},
|
||||
row: {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
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',
|
||||
},
|
||||
});
|
||||
@@ -130,10 +130,7 @@ const styles = StyleSheet.create({
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
paddingBottom: 34, // Safe area
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 12,
|
||||
boxShadow: '0px -4px 12px rgba(0, 0, 0, 0.15)',
|
||||
elevation: 20,
|
||||
},
|
||||
handleContainer: {
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -95,18 +95,12 @@ const styles = StyleSheet.create({
|
||||
// Variants
|
||||
primary: {
|
||||
backgroundColor: KurdistanColors.kesk,
|
||||
shadowColor: KurdistanColors.kesk,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
boxShadow: '0px 4px 8px rgba(0, 128, 0, 0.3)',
|
||||
elevation: 4,
|
||||
},
|
||||
secondary: {
|
||||
backgroundColor: KurdistanColors.zer,
|
||||
shadowColor: KurdistanColors.zer,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 6,
|
||||
boxShadow: '0px 4px 6px rgba(255, 215, 0, 0.2)',
|
||||
elevation: 3,
|
||||
},
|
||||
outline: {
|
||||
@@ -119,10 +113,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
danger: {
|
||||
backgroundColor: KurdistanColors.sor,
|
||||
shadowColor: KurdistanColors.sor,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
boxShadow: '0px 4px 8px rgba(255, 0, 0, 0.3)',
|
||||
elevation: 4,
|
||||
},
|
||||
// Sizes
|
||||
@@ -146,7 +137,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
shadowOpacity: 0,
|
||||
boxShadow: 'none',
|
||||
elevation: 0,
|
||||
},
|
||||
pressed: {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -72,21 +72,18 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 12,
|
||||
},
|
||||
elevated: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
elevation: 4,
|
||||
},
|
||||
outlined: {
|
||||
borderWidth: 1,
|
||||
borderColor: AppColors.border,
|
||||
shadowOpacity: 0,
|
||||
boxShadow: 'none',
|
||||
elevation: 0,
|
||||
},
|
||||
filled: {
|
||||
backgroundColor: AppColors.background,
|
||||
shadowOpacity: 0,
|
||||
boxShadow: 'none',
|
||||
elevation: 0,
|
||||
},
|
||||
pressed: {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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 }],
|
||||
},
|
||||
});
|
||||
@@ -115,10 +115,7 @@ const styles = StyleSheet.create({
|
||||
inputContainerFocused: {
|
||||
borderColor: KurdistanColors.kesk,
|
||||
borderWidth: 2,
|
||||
shadowColor: KurdistanColors.kesk,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
boxShadow: '0px 2px 4px rgba(0, 128, 0, 0.1)',
|
||||
elevation: 2,
|
||||
},
|
||||
inputContainerError: {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
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';
|
||||
|
||||
interface NotificationBellProps {
|
||||
onPress: () => void;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export const NotificationBell: React.FC<NotificationBellProps> = ({ onPress, style }) => {
|
||||
const { selectedAccount, api, isApiReady } = usePezkuwi();
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api || !isApiReady || !selectedAccount) {
|
||||
setUnreadCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch unread notification count from Supabase
|
||||
const fetchUnreadCount = async () => {
|
||||
try {
|
||||
const count = await supabaseHelpers.getUnreadNotificationsCount(selectedAccount.address);
|
||||
setUnreadCount(count);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch unread count:', error);
|
||||
// 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',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,455 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Modal,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
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 { 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}>
|
||||
{/* 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'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',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,406 @@
|
||||
import React, { useRef, useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ActivityIndicator,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
BackHandler,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { KurdistanColors } from '../theme/colors';
|
||||
import { usePezkuwi } from '../contexts/PezkuwiContext';
|
||||
|
||||
// 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 { selectedAccount, getKeyPair } = usePezkuwi();
|
||||
|
||||
// JavaScript to inject into the WebView
|
||||
// This creates a bridge between the web app and native app
|
||||
const injectedJavaScript = `
|
||||
(function() {
|
||||
// Mark this as mobile app
|
||||
window.PEZKUWI_MOBILE = true;
|
||||
window.PEZKUWI_PLATFORM = '${Platform.OS}';
|
||||
|
||||
// Inject wallet address if connected
|
||||
${selectedAccount ? `window.PEZKUWI_ADDRESS = '${selectedAccount.address}';` : ''}
|
||||
${selectedAccount ? `window.PEZKUWI_ACCOUNT_NAME = '${selectedAccount.meta?.name || 'Mobile Wallet'}';` : ''}
|
||||
|
||||
// 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 from native wallet
|
||||
signTransaction: function(extrinsicHex, callback) {
|
||||
window.__pendingSignCallback = callback;
|
||||
window.ReactNativeWebView?.postMessage(JSON.stringify({
|
||||
type: 'SIGN_TRANSACTION',
|
||||
payload: { extrinsicHex }
|
||||
}));
|
||||
},
|
||||
|
||||
// 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
|
||||
if (!selectedAccount) {
|
||||
// Send error back to WebView
|
||||
webViewRef.current?.injectJavaScript(`
|
||||
if (window.__pendingSignCallback) {
|
||||
window.__pendingSignCallback(null, 'Wallet not connected');
|
||||
delete window.__pendingSignCallback;
|
||||
}
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { extrinsicHex } = message.payload as { extrinsicHex: string };
|
||||
const keyPair = await getKeyPair(selectedAccount.address);
|
||||
|
||||
if (!keyPair) {
|
||||
throw new Error('Could not retrieve key pair');
|
||||
}
|
||||
|
||||
// Sign the transaction
|
||||
const signature = keyPair.sign(extrinsicHex);
|
||||
const signatureHex = Buffer.from(signature).toString('hex');
|
||||
|
||||
// Send signature back to WebView
|
||||
webViewRef.current?.injectJavaScript(`
|
||||
if (window.__pendingSignCallback) {
|
||||
window.__pendingSignCallback('${signatureHex}', null);
|
||||
delete window.__pendingSignCallback;
|
||||
}
|
||||
`);
|
||||
} catch (signError) {
|
||||
webViewRef.current?.injectJavaScript(`
|
||||
if (window.__pendingSignCallback) {
|
||||
window.__pendingSignCallback(null, '${(signError as Error).message}');
|
||||
delete window.__pendingSignCallback;
|
||||
}
|
||||
`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'CONNECT_WALLET':
|
||||
// Trigger native wallet connection
|
||||
// This would open a modal or navigate to wallet screen
|
||||
if (__DEV__) console.log('WebView requested wallet connection');
|
||||
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.log('[WebView]:', message.payload);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (__DEV__) {
|
||||
console.log('Unknown message type:', message.type);
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
if (__DEV__) {
|
||||
console.error('Failed to parse WebView message:', parseError);
|
||||
}
|
||||
}
|
||||
}, [selectedAccount, getKeyPair, canGoBack]);
|
||||
|
||||
// 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}`;
|
||||
|
||||
// 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}
|
||||
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}
|
||||
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;
|
||||
@@ -0,0 +1,215 @@
|
||||
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 "your keys, your coins, your responsibility" 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'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'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;
|
||||
@@ -0,0 +1,249 @@
|
||||
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 ("App"), you agree to be bound by these
|
||||
Terms of Service ("Terms"). 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' 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 "AS IS" 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;
|
||||
@@ -0,0 +1,199 @@
|
||||
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, selectedAccount } = 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();
|
||||
// Assuming rawValidators is a list of validator addresses or objects
|
||||
// This parsing logic will need adjustment based on the exact structure returned
|
||||
for (const rawValidator of rawValidators.toHuman() as any[]) { // Adjust 'any' based on actual type
|
||||
// Placeholder: Assume rawValidator is just an address for now
|
||||
chainValidators.push({
|
||||
address: rawValidator.toString(), // or rawValidator.address if it's an object
|
||||
commission: 0.05, // Placeholder: Fetch actual commission
|
||||
totalStake: '0 HEZ', // Placeholder: Fetch actual stake
|
||||
selfStake: '0 HEZ', // Placeholder: Fetch actual self stake
|
||||
nominators: 0, // Placeholder: Fetch actual nominators
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fallback to general staking validators if validatorPool pallet is not found/used
|
||||
const rawStakingValidators = await api.query.staking.validators();
|
||||
for (const validatorAddress of rawStakingValidators.keys) {
|
||||
const address = validatorAddress.args[0].toString();
|
||||
// Fetch more details about each validator if needed, e.g., commission, total stake
|
||||
const validatorPrefs = await api.query.staking.validators(address);
|
||||
const commission = validatorPrefs.commission.toNumber() / 10_000_000; // Assuming 10^7 for percentage
|
||||
|
||||
// For simplicity, total stake and nominators are placeholders for now
|
||||
// A more complete implementation would query for detailed exposure
|
||||
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',
|
||||
},
|
||||
});
|
||||
@@ -15,3 +15,5 @@ export { AddressDisplay } from './AddressDisplay';
|
||||
export { BalanceCard } from './BalanceCard';
|
||||
export { TokenSelector } from './TokenSelector';
|
||||
export type { Token } from './TokenSelector';
|
||||
export { default as PezkuwiWebView } from './PezkuwiWebView';
|
||||
export type { PezkuwiWebViewProps } from './PezkuwiWebView';
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { KurdistanColors } from '../../theme/colors';
|
||||
import type { StackHeaderProps } from '@react-navigation/stack';
|
||||
|
||||
interface GradientHeaderProps extends StackHeaderProps {
|
||||
subtitle?: string;
|
||||
rightButtons?: React.ReactNode;
|
||||
gradientColors?: [string, string];
|
||||
}
|
||||
|
||||
export const GradientHeader: React.FC<GradientHeaderProps> = ({
|
||||
navigation,
|
||||
options,
|
||||
route,
|
||||
subtitle,
|
||||
rightButtons,
|
||||
gradientColors = [KurdistanColors.kesk, '#008f43'],
|
||||
}) => {
|
||||
const title = options.headerTitle !== undefined
|
||||
? options.headerTitle
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
const canGoBack = navigation.canGoBack();
|
||||
|
||||
return (
|
||||
<LinearGradient colors={gradientColors} style={styles.gradientHeader}>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerLeft}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<Text style={styles.backButtonText}>←</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.headerTitle}>{title}</Text>
|
||||
{subtitle && <Text style={styles.headerSubtitle}>{subtitle}</Text>}
|
||||
</View>
|
||||
|
||||
<View style={styles.headerRight}>{rightButtons}</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
);
|
||||
};
|
||||
|
||||
interface SimpleHeaderProps extends StackHeaderProps {
|
||||
rightButtons?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SimpleHeader: React.FC<SimpleHeaderProps> = ({
|
||||
navigation,
|
||||
options,
|
||||
route,
|
||||
rightButtons,
|
||||
}) => {
|
||||
const title = options.headerTitle !== undefined
|
||||
? options.headerTitle
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
const canGoBack = navigation.canGoBack();
|
||||
|
||||
return (
|
||||
<View style={styles.simpleHeader}>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerLeft}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<Text style={styles.simpleBackButtonText}>←</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.simpleHeaderTitle}>{title}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerRight}>{rightButtons}</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const BackButton: React.FC<{ onPress?: () => void; color?: string }> = ({
|
||||
onPress,
|
||||
color = '#FFFFFF',
|
||||
}) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} style={styles.backButton}>
|
||||
<Text style={[styles.backButtonText, { color }]}>←</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
interface AppBarHeaderProps extends StackHeaderProps {
|
||||
rightButtons?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AppBarHeader: React.FC<AppBarHeaderProps> = ({
|
||||
navigation,
|
||||
options,
|
||||
route,
|
||||
rightButtons,
|
||||
}) => {
|
||||
const title = options.headerTitle !== undefined
|
||||
? options.headerTitle
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
const canGoBack = navigation.canGoBack();
|
||||
|
||||
return (
|
||||
<View style={styles.appBarHeader}>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerLeft}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<Text style={styles.appBarBackButtonText}>←</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.appBarTitle}>{title}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerRight}>{rightButtons}</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
gradientHeader: {
|
||||
paddingTop: 50,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
simpleHeader: {
|
||||
paddingTop: 50,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E5E5',
|
||||
},
|
||||
headerContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
headerLeft: {
|
||||
width: 60,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
headerCenter: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerRight: {
|
||||
width: 60,
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
headerSubtitle: {
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
marginTop: 2,
|
||||
},
|
||||
simpleHeaderTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: KurdistanColors.reş,
|
||||
},
|
||||
backButton: {
|
||||
padding: 8,
|
||||
},
|
||||
backButtonText: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
simpleBackButtonText: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: KurdistanColors.reş,
|
||||
},
|
||||
appBarHeader: {
|
||||
paddingTop: 50,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
elevation: 4,
|
||||
},
|
||||
appBarTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: KurdistanColors.reş,
|
||||
},
|
||||
appBarBackButtonText: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: KurdistanColors.kesk,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { KurdistanColors } from '../../theme/colors';
|
||||
import type { StackHeaderProps } from '@react-navigation/stack';
|
||||
|
||||
interface GradientHeaderProps extends StackHeaderProps {
|
||||
subtitle?: string;
|
||||
rightButtons?: React.ReactNode;
|
||||
gradientColors?: [string, string];
|
||||
}
|
||||
|
||||
export const GradientHeader: React.FC<GradientHeaderProps> = ({
|
||||
navigation,
|
||||
options,
|
||||
route,
|
||||
subtitle,
|
||||
rightButtons,
|
||||
gradientColors = [KurdistanColors.kesk, '#008f43'],
|
||||
}) => {
|
||||
const title = options.headerTitle !== undefined
|
||||
? options.headerTitle
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
const canGoBack = navigation.canGoBack();
|
||||
|
||||
return (
|
||||
<LinearGradient colors={gradientColors} style={styles.gradientHeader}>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerLeft}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<Text style={styles.backButtonText}>←</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.headerTitle}>{title}</Text>
|
||||
{subtitle && <Text style={styles.headerSubtitle}>{subtitle}</Text>}
|
||||
</View>
|
||||
|
||||
<View style={styles.headerRight}>{rightButtons}</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
);
|
||||
};
|
||||
|
||||
interface SimpleHeaderProps extends StackHeaderProps {
|
||||
rightButtons?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SimpleHeader: React.FC<SimpleHeaderProps> = ({
|
||||
navigation,
|
||||
options,
|
||||
route,
|
||||
rightButtons,
|
||||
}) => {
|
||||
const title = options.headerTitle !== undefined
|
||||
? options.headerTitle
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
const canGoBack = navigation.canGoBack();
|
||||
|
||||
return (
|
||||
<View style={styles.simpleHeader}>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerLeft}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<Text style={styles.simpleBackButtonText}>←</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.simpleHeaderTitle}>{title}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerRight}>{rightButtons}</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const BackButton: React.FC<{ onPress?: () => void; color?: string }> = ({
|
||||
onPress,
|
||||
color = '#FFFFFF',
|
||||
}) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} style={styles.backButton}>
|
||||
<Text style={[styles.backButtonText, { color }]}>←</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
interface AppBarHeaderProps extends StackHeaderProps {
|
||||
rightButtons?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AppBarHeader: React.FC<AppBarHeaderProps> = ({
|
||||
navigation,
|
||||
options,
|
||||
route,
|
||||
rightButtons,
|
||||
}) => {
|
||||
const title = options.headerTitle !== undefined
|
||||
? options.headerTitle
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
const canGoBack = navigation.canGoBack();
|
||||
|
||||
return (
|
||||
<View style={styles.appBarHeader}>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerLeft}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<Text style={styles.appBarBackButtonText}>←</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.appBarTitle}>{title}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerRight}>{rightButtons}</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
gradientHeader: {
|
||||
paddingTop: 50,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
simpleHeader: {
|
||||
paddingTop: 50,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E5E5',
|
||||
},
|
||||
headerContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
headerLeft: {
|
||||
width: 60,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
headerCenter: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerRight: {
|
||||
width: 60,
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
headerSubtitle: {
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
marginTop: 2,
|
||||
},
|
||||
simpleHeaderTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: KurdistanColors.reş,
|
||||
},
|
||||
backButton: {
|
||||
padding: 8,
|
||||
},
|
||||
backButtonText: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
simpleBackButtonText: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: KurdistanColors.reş,
|
||||
},
|
||||
appBarHeader: {
|
||||
paddingTop: 50,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 4,
|
||||
},
|
||||
appBarTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: KurdistanColors.reş,
|
||||
},
|
||||
appBarBackButtonText: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: KurdistanColors.kesk,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,411 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Modal,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
ScrollView,
|
||||
Share,
|
||||
Clipboard,
|
||||
Alert,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { KurdistanColors } from '../../theme/colors';
|
||||
import { usePezkuwi } from '../../contexts/PezkuwiContext';
|
||||
|
||||
interface InviteModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const InviteModal: React.FC<InviteModalProps> = ({ visible, onClose }) => {
|
||||
const { selectedAccount, api } = usePezkuwi();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [inviteeAddress, setInviteeAddress] = useState('');
|
||||
|
||||
// Generate referral link
|
||||
const referralLink = useMemo(() => {
|
||||
if (!selectedAccount?.address) return '';
|
||||
// TODO: Update with actual app deep link or web URL
|
||||
return `https://pezkuwi.net/be-citizen?ref=${selectedAccount.address}`;
|
||||
}, [selectedAccount?.address]);
|
||||
|
||||
const shareText = useMemo(() => {
|
||||
return `Join me on Digital Kurdistan (PezkuwiChain)! 🏛️\n\nBecome a citizen and get your Welati Tiki NFT.\n\nUse my referral link:\n${referralLink}`;
|
||||
}, [referralLink]);
|
||||
|
||||
const handleCopy = () => {
|
||||
Clipboard.setString(referralLink);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
Alert.alert('Copied!', 'Referral link copied to clipboard');
|
||||
};
|
||||
|
||||
const handleNativeShare = async () => {
|
||||
try {
|
||||
await Share.share({
|
||||
message: shareText,
|
||||
title: 'Join Digital Kurdistan',
|
||||
});
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Share error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSharePlatform = (platform: string) => {
|
||||
const encodedText = encodeURIComponent(shareText);
|
||||
const encodedUrl = encodeURIComponent(referralLink);
|
||||
|
||||
const urls: Record<string, string> = {
|
||||
whatsapp: `whatsapp://send?text=${encodedText}`,
|
||||
telegram: `tg://msg?text=${encodedText}`,
|
||||
twitter: `twitter://post?message=${encodedText}`,
|
||||
email: `mailto:?subject=${encodeURIComponent('Join Digital Kurdistan')}&body=${encodedText}`,
|
||||
};
|
||||
|
||||
if (urls[platform]) {
|
||||
Linking.openURL(urls[platform]).catch(() => {
|
||||
// Fallback to web URL if app not installed
|
||||
const webUrls: Record<string, string> = {
|
||||
whatsapp: `https://wa.me/?text=${encodedText}`,
|
||||
telegram: `https://t.me/share/url?url=${encodedUrl}&text=${encodeURIComponent('Join Digital Kurdistan! 🏛️')}`,
|
||||
twitter: `https://twitter.com/intent/tweet?text=${encodedText}`,
|
||||
};
|
||||
if (webUrls[platform]) {
|
||||
Linking.openURL(webUrls[platform]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleInitiateReferral = async () => {
|
||||
if (!api || !selectedAccount || !inviteeAddress) {
|
||||
Alert.alert('Error', 'Please enter a valid address');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Implement on-chain referral initiation
|
||||
// const tx = api.tx.referral.initiateReferral(inviteeAddress);
|
||||
// await tx.signAndSend(selectedAccount.address);
|
||||
Alert.alert('Success', 'Referral initiated successfully!');
|
||||
setInviteeAddress('');
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Initiate referral error:', error);
|
||||
Alert.alert('Error', 'Failed to initiate referral');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
{/* Header */}
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Invite Friends</Text>
|
||||
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
||||
<Text style={styles.closeButtonText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.modalBody} showsVerticalScrollIndicator={false}>
|
||||
<Text style={styles.modalDescription}>
|
||||
Share your referral link. When your friends complete KYC, you'll earn trust score points!
|
||||
</Text>
|
||||
|
||||
{/* Referral Link */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionLabel}>Your Referral Link</Text>
|
||||
<View style={styles.linkContainer}>
|
||||
<TextInput
|
||||
style={styles.linkInput}
|
||||
value={referralLink}
|
||||
editable={false}
|
||||
multiline
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.copyButton} onPress={handleCopy}>
|
||||
<Text style={styles.copyButtonIcon}>{copied ? '✓' : '📋'}</Text>
|
||||
<Text style={styles.copyButtonText}>{copied ? 'Copied!' : 'Copy Link'}</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.hint}>
|
||||
Anyone who signs up with this link will be counted as your referral
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Share Options */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionLabel}>Share via</Text>
|
||||
<View style={styles.shareGrid}>
|
||||
<TouchableOpacity
|
||||
style={styles.shareButton}
|
||||
onPress={handleNativeShare}
|
||||
>
|
||||
<Text style={styles.shareButtonIcon}>📤</Text>
|
||||
<Text style={styles.shareButtonText}>Share</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.shareButton}
|
||||
onPress={() => handleSharePlatform('whatsapp')}
|
||||
>
|
||||
<Text style={styles.shareButtonIcon}>💬</Text>
|
||||
<Text style={styles.shareButtonText}>WhatsApp</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.shareButton}
|
||||
onPress={() => handleSharePlatform('telegram')}
|
||||
>
|
||||
<Text style={styles.shareButtonIcon}>✈️</Text>
|
||||
<Text style={styles.shareButtonText}>Telegram</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.shareButton}
|
||||
onPress={() => handleSharePlatform('twitter')}
|
||||
>
|
||||
<Text style={styles.shareButtonIcon}>🐦</Text>
|
||||
<Text style={styles.shareButtonText}>Twitter</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.shareButton}
|
||||
onPress={() => handleSharePlatform('email')}
|
||||
>
|
||||
<Text style={styles.shareButtonIcon}>📧</Text>
|
||||
<Text style={styles.shareButtonText}>Email</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Advanced: Pre-register */}
|
||||
<View style={[styles.section, styles.advancedSection]}>
|
||||
<Text style={styles.sectionLabel}>Pre-Register a Friend (Advanced)</Text>
|
||||
<Text style={styles.hint}>
|
||||
If you know your friend's wallet address, you can pre-register them on-chain.
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.addressInput}
|
||||
placeholder="Friend's wallet address"
|
||||
placeholderTextColor="#999"
|
||||
value={inviteeAddress}
|
||||
onChangeText={setInviteeAddress}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.initiateButton}
|
||||
onPress={handleInitiateReferral}
|
||||
disabled={!inviteeAddress}
|
||||
>
|
||||
<Text style={styles.initiateButtonText}>Initiate Referral</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Rewards Info */}
|
||||
<View style={styles.rewardsSection}>
|
||||
<Text style={styles.rewardsSectionTitle}>Referral Rewards</Text>
|
||||
<View style={styles.rewardRow}>
|
||||
<Text style={styles.rewardText}>• 1-10 referrals: 10 points each (up to 100)</Text>
|
||||
</View>
|
||||
<View style={styles.rewardRow}>
|
||||
<Text style={styles.rewardText}>• 11-50 referrals: 5 points each (up to 300)</Text>
|
||||
</View>
|
||||
<View style={styles.rewardRow}>
|
||||
<Text style={styles.rewardText}>• 51-100 referrals: 4 points each (up to 500)</Text>
|
||||
</View>
|
||||
<View style={styles.rewardRow}>
|
||||
<Text style={styles.rewardText}>• Maximum: 500 trust score points</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Footer */}
|
||||
<TouchableOpacity style={styles.doneButton} onPress={onClose}>
|
||||
<Text style={styles.doneButtonText}>Done</Text>
|
||||
</TouchableOpacity>
|
||||
</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: '90%',
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#F0F0F0',
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
},
|
||||
closeButton: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F5F5F5',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
closeButtonText: {
|
||||
fontSize: 18,
|
||||
color: '#666',
|
||||
},
|
||||
modalBody: {
|
||||
padding: 20,
|
||||
},
|
||||
modalDescription: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
marginBottom: 20,
|
||||
lineHeight: 20,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
},
|
||||
linkContainer: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
linkInput: {
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
copyButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: KurdistanColors.kesk,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
gap: 8,
|
||||
},
|
||||
copyButtonIcon: {
|
||||
fontSize: 16,
|
||||
},
|
||||
copyButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
hint: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
lineHeight: 16,
|
||||
},
|
||||
shareGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
},
|
||||
shareButton: {
|
||||
flex: 1,
|
||||
minWidth: '30%',
|
||||
alignItems: 'center',
|
||||
padding: 12,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderRadius: 12,
|
||||
gap: 4,
|
||||
},
|
||||
shareButtonIcon: {
|
||||
fontSize: 24,
|
||||
},
|
||||
shareButtonText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#666',
|
||||
},
|
||||
advancedSection: {
|
||||
backgroundColor: '#E0F2FE',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 24,
|
||||
},
|
||||
addressInput: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
color: '#333',
|
||||
marginTop: 8,
|
||||
marginBottom: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#D1D5DB',
|
||||
},
|
||||
initiateButton: {
|
||||
backgroundColor: '#3B82F6',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
initiateButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
rewardsSection: {
|
||||
backgroundColor: 'rgba(0, 143, 67, 0.1)',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
},
|
||||
rewardsSectionTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: KurdistanColors.kesk,
|
||||
marginBottom: 12,
|
||||
},
|
||||
rewardRow: {
|
||||
marginBottom: 6,
|
||||
},
|
||||
rewardText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
},
|
||||
doneButton: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
paddingVertical: 16,
|
||||
marginHorizontal: 20,
|
||||
marginBottom: 20,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
doneButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Modal,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { KurdistanColors } from '../../theme/colors';
|
||||
import { usePezkuwi } from '../../contexts/PezkuwiContext';
|
||||
|
||||
interface AddTokenModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onTokenAdded?: () => void;
|
||||
}
|
||||
|
||||
export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onTokenAdded,
|
||||
}) => {
|
||||
const { api, isApiReady } = usePezkuwi();
|
||||
const [assetId, setAssetId] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [tokenMetadata, setTokenMetadata] = useState<{
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
name?: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleFetchMetadata = async () => {
|
||||
if (!api || !isApiReady) {
|
||||
Alert.alert('Error', 'API not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assetId || isNaN(Number(assetId))) {
|
||||
Alert.alert('Error', 'Please enter a valid asset ID');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const assetIdNum = Number(assetId);
|
||||
|
||||
// Fetch asset metadata
|
||||
const metadataOption = await api.query.assets.metadata(assetIdNum);
|
||||
|
||||
if (metadataOption.isEmpty) {
|
||||
Alert.alert('Error', 'Asset not found');
|
||||
setTokenMetadata(null);
|
||||
} else {
|
||||
const metadata = metadataOption.toJSON() as any;
|
||||
setTokenMetadata({
|
||||
symbol: metadata.symbol || 'UNKNOWN',
|
||||
decimals: metadata.decimals || 12,
|
||||
name: metadata.name || 'Unknown Token',
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch token metadata:', error);
|
||||
Alert.alert('Error', 'Failed to fetch token metadata');
|
||||
setTokenMetadata(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddToken = () => {
|
||||
if (!tokenMetadata) {
|
||||
Alert.alert('Error', 'Please fetch token metadata first');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the custom token in AsyncStorage or app state
|
||||
// For now, just show success and call the callback
|
||||
Alert.alert(
|
||||
'Success',
|
||||
`Token ${tokenMetadata.symbol} (ID: ${assetId}) added to your wallet!`,
|
||||
[
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
handleClose();
|
||||
if (onTokenAdded) onTokenAdded();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAssetId('');
|
||||
setTokenMetadata(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="slide"
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalCard}>
|
||||
<Text style={styles.modalHeader}>Add Custom Token</Text>
|
||||
|
||||
<Text style={styles.instructions}>
|
||||
Enter the asset ID to add a custom token to your wallet
|
||||
</Text>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.inputField}
|
||||
placeholder="Asset ID (e.g., 1000)"
|
||||
keyboardType="numeric"
|
||||
value={assetId}
|
||||
onChangeText={setAssetId}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.fetchButton}
|
||||
onPress={handleFetchMetadata}
|
||||
disabled={isLoading || !assetId}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<Text style={styles.fetchButtonText}>Fetch</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{tokenMetadata && (
|
||||
<View style={styles.metadataContainer}>
|
||||
<View style={styles.metadataRow}>
|
||||
<Text style={styles.metadataLabel}>Symbol:</Text>
|
||||
<Text style={styles.metadataValue}>{tokenMetadata.symbol}</Text>
|
||||
</View>
|
||||
<View style={styles.metadataRow}>
|
||||
<Text style={styles.metadataLabel}>Name:</Text>
|
||||
<Text style={styles.metadataValue}>{tokenMetadata.name}</Text>
|
||||
</View>
|
||||
<View style={styles.metadataRow}>
|
||||
<Text style={styles.metadataLabel}>Decimals:</Text>
|
||||
<Text style={styles.metadataValue}>{tokenMetadata.decimals}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity style={styles.btnCancel} onPress={handleClose}>
|
||||
<Text style={styles.btnCancelText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.btnConfirm,
|
||||
!tokenMetadata && styles.btnConfirmDisabled,
|
||||
]}
|
||||
onPress={handleAddToken}
|
||||
disabled={!tokenMetadata}
|
||||
>
|
||||
<Text style={styles.btnConfirmText}>Add Token</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
modalCard: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 20,
|
||||
padding: 24,
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
},
|
||||
modalHeader: {
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
instructions: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
marginBottom: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputField: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
fontSize: 16,
|
||||
},
|
||||
fetchButton: {
|
||||
backgroundColor: KurdistanColors.kesk,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minWidth: 80,
|
||||
},
|
||||
fetchButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
metadataContainer: {
|
||||
backgroundColor: '#F9F9F9',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
},
|
||||
metadataRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 8,
|
||||
},
|
||||
metadataLabel: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '500',
|
||||
},
|
||||
metadataValue: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
fontWeight: '600',
|
||||
},
|
||||
modalActions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
btnCancel: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#EEE',
|
||||
alignItems: 'center',
|
||||
},
|
||||
btnCancelText: {
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
fontWeight: '600',
|
||||
},
|
||||
btnConfirm: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
backgroundColor: KurdistanColors.kesk,
|
||||
alignItems: 'center',
|
||||
},
|
||||
btnConfirmDisabled: {
|
||||
backgroundColor: '#CCC',
|
||||
},
|
||||
btnConfirmText: {
|
||||
fontSize: 16,
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user