feat: add Telegram mini app connect for P2P access

This commit is contained in:
2026-01-29 21:27:13 +03:00
parent 3879780c14
commit dd1e0dc294
48 changed files with 2558 additions and 9502 deletions
+62 -3
View File
@@ -54,6 +54,7 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
const { user } = useAuth();
const [sessionToken, setSessionToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
const [isSessionReady, setIsSessionReady] = useState(false);
// Get Supabase session token for WebView authentication
React.useEffect(() => {
@@ -63,9 +64,12 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
if (session?.access_token) {
setSessionToken(session.access_token);
setRefreshToken(session.refresh_token || null);
if (__DEV__) console.log('[WebView] Session token retrieved for SSO');
}
} catch (error) {
if (__DEV__) console.warn('[WebView] Failed to get session:', error);
} finally {
setIsSessionReady(true);
}
};
getSession();
@@ -89,6 +93,31 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
${user ? `window.PEZKUWI_USER_ID = '${user.id}';` : ''}
${user?.email ? `window.PEZKUWI_USER_EMAIL = '${user.email}';` : ''}
// Pre-populate localStorage with session so Supabase client finds it on init
${sessionToken && user ? `
try {
var supabaseUrl = 'https://sihawipngjtgvfzukfew.supabase.co';
var storageKey = 'sb-' + supabaseUrl.replace('https://', '').split('.')[0] + '-auth-token';
var sessionData = {
access_token: '${sessionToken}',
refresh_token: '${refreshToken || ''}',
token_type: 'bearer',
expires_in: 3600,
expires_at: Math.floor(Date.now() / 1000) + 3600,
user: {
id: '${user.id}',
email: '${user.email || ''}',
aud: 'authenticated',
role: 'authenticated'
}
};
localStorage.setItem(storageKey, JSON.stringify(sessionData));
console.log('[Mobile] Pre-populated localStorage with session');
} catch(e) {
console.warn('[Mobile] Failed to set localStorage:', e);
}
` : ''}
// Auto-authenticate with Supabase if session token exists
if (window.PEZKUWI_SESSION_TOKEN) {
(function autoAuth(attempts = 0) {
@@ -96,14 +125,25 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
console.warn('[Mobile] Auto-auth timed out: window.supabase not found');
return;
}
if (window.supabase && window.supabase.auth) {
window.supabase.auth.setSession({
access_token: window.PEZKUWI_SESSION_TOKEN,
refresh_token: window.PEZKUWI_REFRESH_TOKEN || ''
}).then(function(res) {
if (res.error) console.warn('[Mobile] Auto-auth error:', res.error);
else console.log('[Mobile] Auto-authenticated successfully');
if (res.error) {
console.warn('[Mobile] Auto-auth error:', res.error);
} else {
console.log('[Mobile] Auto-authenticated successfully');
// Dispatch event to notify app of successful auth
window.dispatchEvent(new CustomEvent('pezkuwi-session-restored', {
detail: { userId: window.PEZKUWI_USER_ID }
}));
// Force auth state refresh if the app has an auth store
if (window.__refreshAuthState) {
window.__refreshAuthState();
}
}
}).catch(function(err) {
console.warn('[Mobile] Auto-auth promise failed:', err);
});
@@ -367,6 +407,25 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
// Build the full URL
const fullUrl = `${WEB_BASE_URL}${path}`;
// Wait for session to be ready before loading WebView (ensures SSO works)
if (!isSessionReady) {
return (
<View style={styles.container}>
{title && (
<View style={styles.header}>
<View style={{ width: 40 }} />
<Text style={styles.headerTitle}>{title}</Text>
<View style={{ width: 40 }} />
</View>
)}
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<Text style={styles.loadingText}>Preparing session...</Text>
</View>
</View>
);
}
// Error view
if (error) {
return (
+17 -1
View File
@@ -12,6 +12,7 @@ import {
Platform,
KeyboardAvoidingView,
AlertButton,
Image,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useAuth } from '../contexts/AuthContext';
@@ -74,6 +75,12 @@ const getEmojiFromAvatarId = (avatarId: string): string => {
return avatar ? avatar.emoji : '👤';
};
// Check if avatar is an uploaded image URL vs emoji avatar ID
const isUploadedImageUrl = (avatarUrl: string | null): boolean => {
if (!avatarUrl) return false;
return avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://');
};
const EditProfileScreen: React.FC = () => {
const navigation = useNavigation();
const { user } = useAuth();
@@ -242,7 +249,11 @@ const EditProfileScreen: React.FC = () => {
>
<View style={[styles.avatarCircle, { backgroundColor: colors.surface }]}>
{avatarUrl ? (
<Text style={styles.avatarEmoji}>{getEmojiFromAvatarId(avatarUrl)}</Text>
isUploadedImageUrl(avatarUrl) ? (
<Image source={{ uri: avatarUrl }} style={styles.avatarImage} />
) : (
<Text style={styles.avatarEmoji}>{getEmojiFromAvatarId(avatarUrl)}</Text>
)
) : (
<Text style={[styles.avatarInitial, { color: colors.textSecondary }]}>
{fullName?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || '?'}
@@ -382,6 +393,11 @@ const styles = StyleSheet.create({
avatarEmoji: {
fontSize: 70,
},
avatarImage: {
width: 120,
height: 120,
borderRadius: 60,
},
avatarInitial: {
fontSize: 48,
fontWeight: 'bold',
+70 -43
View File
@@ -392,16 +392,9 @@ const SettingsScreen: React.FC = () => {
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="settings-screen">
<View style={[styles.container, { backgroundColor: colors.background }]} testID="settings-screen">
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
{/* Header */}
<View style={[styles.header, { backgroundColor: colors.surface, borderBottomColor: colors.border }]}>
<View style={{ width: 40 }} />
<Text style={[styles.headerTitle, { color: colors.text }]}>Settings</Text>
<View style={{ width: 40 }} />
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{/* ACCOUNT SECTION */}
@@ -526,42 +519,76 @@ const SettingsScreen: React.FC = () => {
{/* DEVELOPER OPTIONS (only in DEV) */}
{__DEV__ && (
<View style={[styles.section, { backgroundColor: colors.surface, marginTop: 20 }]}>
<Text style={[styles.sectionHeader, { color: colors.textSecondary }]}>DEVELOPER</Text>
<SettingItem
icon="🗑️"
title="Reset Wallet"
subtitle="Clear all wallet data"
textColor="#FF9500"
showArrow={false}
onPress={() => {
showAlert(
'Reset Wallet',
'This will delete all wallet data including saved accounts and keys. Are you sure?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Reset',
style: 'destructive',
onPress: async () => {
try {
await AsyncStorage.multiRemove([
'@pezkuwi_wallets',
'@pezkuwi_selected_account',
'@pezkuwi_selected_network'
]);
showAlert('Success', 'Wallet data cleared. Restart the app to see changes.');
} catch {
showAlert('Error', 'Failed to clear wallet data');
<>
<SectionHeader title="DEVELOPER" />
<View style={[styles.section, { backgroundColor: colors.surface }]}>
<SettingItem
icon="🔄"
title="Reset Onboarding"
subtitle="Show Welcome & Verify screens again"
textColor="#FF9500"
showArrow={false}
onPress={() => {
showAlert(
'Reset Onboarding',
'This will reset onboarding state. Restart the app to see the Welcome screen.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Reset',
style: 'destructive',
onPress: async () => {
try {
await AsyncStorage.multiRemove([
'@pezkuwi/privacy_consent_accepted',
'@pezkuwi_human_verified'
]);
showAlert('Success', 'Onboarding reset. Restart the app to see changes.');
} catch {
showAlert('Error', 'Failed to reset onboarding');
}
}
}
}
]
);
}}
testID="reset-wallet-button"
/>
</View>
]
);
}}
testID="reset-onboarding-button"
/>
<SettingItem
icon="🗑️"
title="Reset Wallet"
subtitle="Clear all wallet data"
textColor="#FF9500"
showArrow={false}
onPress={() => {
showAlert(
'Reset Wallet',
'This will delete all wallet data including saved accounts and keys. Are you sure?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Reset',
style: 'destructive',
onPress: async () => {
try {
await AsyncStorage.multiRemove([
'@pezkuwi_wallets',
'@pezkuwi_selected_account',
'@pezkuwi_selected_network'
]);
showAlert('Success', 'Wallet data cleared. Restart the app to see changes.');
} catch {
showAlert('Error', 'Failed to clear wallet data');
}
}
}
]
);
}}
testID="reset-wallet-button"
/>
</View>
</>
)}
{/* LOGOUT */}
@@ -925,7 +952,7 @@ const SettingsScreen: React.FC = () => {
</SafeAreaView>
</Modal>
</SafeAreaView>
</View>
);
};