Update beneficiaries layout and screens
This commit is contained in:
parent
7186f29f35
commit
ad35dac850
@ -60,10 +60,22 @@ export default function ActivateScreen() {
|
|||||||
try {
|
try {
|
||||||
// If we have an existing beneficiary, update them with device info
|
// If we have an existing beneficiary, update them with device info
|
||||||
if (existingBeneficiaryId && existingBeneficiary) {
|
if (existingBeneficiaryId && existingBeneficiary) {
|
||||||
await updateLocalBeneficiary(existingBeneficiaryId, {
|
// Prepare update data
|
||||||
|
const updateData: any = {
|
||||||
hasDevices: true,
|
hasDevices: true,
|
||||||
device_id: code,
|
device_id: code,
|
||||||
});
|
equipmentStatus: 'active', // Clear awaiting state - sensors now active
|
||||||
|
};
|
||||||
|
|
||||||
|
// If beneficiary has pending subscription (from kit purchase), activate it
|
||||||
|
if (existingBeneficiary.subscription?.status === 'pending') {
|
||||||
|
updateData.subscription = {
|
||||||
|
...existingBeneficiary.subscription,
|
||||||
|
status: 'active',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateLocalBeneficiary(existingBeneficiaryId, updateData);
|
||||||
setBeneficiaryName(existingBeneficiary.name);
|
setBeneficiaryName(existingBeneficiary.name);
|
||||||
setStep('complete');
|
setStep('complete');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,864 +0,0 @@
|
|||||||
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Modal, TextInput, Image, ScrollView, KeyboardAvoidingView, Platform, Alert, Animated } from 'react-native';
|
|
||||||
import { WebView } from 'react-native-webview';
|
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { useLocalSearchParams, router, useFocusEffect } from 'expo-router';
|
|
||||||
import * as SecureStore from 'expo-secure-store';
|
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
|
||||||
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
|
||||||
import { api } from '@/services/api';
|
|
||||||
import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, Shadows, AvatarSizes } from '@/constants/theme';
|
|
||||||
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
|
||||||
import { useToast } from '@/components/ui/Toast';
|
|
||||||
import MockDashboard from '@/components/MockDashboard';
|
|
||||||
import { SubscriptionPayment } from '@/components/SubscriptionPayment';
|
|
||||||
import type { Beneficiary } from '@/types';
|
|
||||||
|
|
||||||
// Dashboard URL with beneficiary ID (deployment_id)
|
|
||||||
const getDashboardUrl = (deploymentId: string) =>
|
|
||||||
`https://react.eluxnetworks.net/dashboard/${deploymentId}`;
|
|
||||||
|
|
||||||
// Local beneficiaries have timestamp-based IDs (>1000000000)
|
|
||||||
// Real deployments have small IDs (21, 38, 29, etc.)
|
|
||||||
const isLocalBeneficiary = (id: string | number): boolean => {
|
|
||||||
const numId = typeof id === 'string' ? parseInt(id, 10) : id;
|
|
||||||
return numId > 1000000000;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function BeneficiaryDashboardScreen() {
|
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
|
||||||
const { currentBeneficiary, setCurrentBeneficiary, localBeneficiaries, updateLocalBeneficiary } = useBeneficiary();
|
|
||||||
const toast = useToast();
|
|
||||||
const webViewRef = useRef<WebView>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [canGoBack, setCanGoBack] = useState(false);
|
|
||||||
const [authToken, setAuthToken] = useState<string | null>(null);
|
|
||||||
const [userName, setUserName] = useState<string | null>(null);
|
|
||||||
const [userId, setUserId] = useState<string | null>(null);
|
|
||||||
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
|
|
||||||
const [isMenuVisible, setIsMenuVisible] = useState(false);
|
|
||||||
|
|
||||||
// Edit modal state
|
|
||||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
|
||||||
const [editForm, setEditForm] = useState({
|
|
||||||
name: '',
|
|
||||||
address: '',
|
|
||||||
avatar: '' as string | undefined,
|
|
||||||
});
|
|
||||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
||||||
|
|
||||||
// Beneficiary data for subscription check
|
|
||||||
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
|
|
||||||
const [isBeneficiaryLoading, setIsBeneficiaryLoading] = useState(true);
|
|
||||||
|
|
||||||
// Check if this is a local (mock) beneficiary
|
|
||||||
const isLocal = useMemo(() => id ? isLocalBeneficiary(id) : false, [id]);
|
|
||||||
|
|
||||||
// Check subscription status
|
|
||||||
const hasActiveSubscription = useMemo(() => {
|
|
||||||
if (!beneficiary) return false;
|
|
||||||
const subscription = beneficiary.subscription;
|
|
||||||
return subscription && subscription.status === 'active';
|
|
||||||
}, [beneficiary]);
|
|
||||||
|
|
||||||
// Load beneficiary data to check subscription
|
|
||||||
const loadBeneficiary = useCallback(async () => {
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
setIsBeneficiaryLoading(true);
|
|
||||||
try {
|
|
||||||
if (isLocal) {
|
|
||||||
const localBeneficiary = localBeneficiaries.find(
|
|
||||||
(b) => b.id === parseInt(id, 10)
|
|
||||||
);
|
|
||||||
if (localBeneficiary) {
|
|
||||||
setBeneficiary(localBeneficiary);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const response = await api.getBeneficiary(parseInt(id, 10));
|
|
||||||
if (response.ok && response.data) {
|
|
||||||
setBeneficiary(response.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load beneficiary:', err);
|
|
||||||
} finally {
|
|
||||||
setIsBeneficiaryLoading(false);
|
|
||||||
}
|
|
||||||
}, [id, isLocal, localBeneficiaries]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadBeneficiary();
|
|
||||||
}, [loadBeneficiary]);
|
|
||||||
|
|
||||||
// Edit modal animation
|
|
||||||
useEffect(() => {
|
|
||||||
Animated.timing(fadeAnim, {
|
|
||||||
toValue: isEditModalVisible ? 1 : 0,
|
|
||||||
duration: 250,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
}, [isEditModalVisible]);
|
|
||||||
|
|
||||||
// Hide menu when navigating away from page
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
return () => {
|
|
||||||
setIsMenuVisible(false);
|
|
||||||
setIsEditModalVisible(false);
|
|
||||||
};
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleEditPress = () => {
|
|
||||||
if (beneficiary) {
|
|
||||||
setEditForm({
|
|
||||||
name: beneficiary.name || '',
|
|
||||||
address: beneficiary.address || '',
|
|
||||||
avatar: beneficiary.avatar,
|
|
||||||
});
|
|
||||||
setIsEditModalVisible(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePickAvatar = async () => {
|
|
||||||
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
||||||
if (status !== 'granted') {
|
|
||||||
Alert.alert('Permission needed', 'Please allow access to your photo library.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await ImagePicker.launchImageLibraryAsync({
|
|
||||||
mediaTypes: ['images'],
|
|
||||||
allowsEditing: true,
|
|
||||||
aspect: [1, 1],
|
|
||||||
quality: 0.5,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.canceled && result.assets[0]) {
|
|
||||||
setEditForm(prev => ({ ...prev, avatar: result.assets[0].uri }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveEdit = async () => {
|
|
||||||
if (!editForm.name.trim()) {
|
|
||||||
toast.error('Error', 'Name is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLocal && id) {
|
|
||||||
const updated = await updateLocalBeneficiary(parseInt(id, 10), {
|
|
||||||
name: editForm.name.trim(),
|
|
||||||
address: editForm.address.trim() || undefined,
|
|
||||||
avatar: editForm.avatar,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
setBeneficiary(updated);
|
|
||||||
setCurrentBeneficiary(updated);
|
|
||||||
setIsEditModalVisible(false);
|
|
||||||
toast.success('Profile Updated', 'Changes saved successfully');
|
|
||||||
} else {
|
|
||||||
toast.error('Error', 'Failed to save changes.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For API beneficiaries - would call backend here
|
|
||||||
toast.info('Coming Soon', 'Editing requires backend API.');
|
|
||||||
setIsEditModalVisible(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build dashboard URL with beneficiary ID
|
|
||||||
const dashboardUrl = id ? getDashboardUrl(id) : 'https://react.eluxnetworks.net/dashboard';
|
|
||||||
|
|
||||||
const beneficiaryName = currentBeneficiary?.name || 'Dashboard';
|
|
||||||
|
|
||||||
// Load token, username, and userId from SecureStore
|
|
||||||
useEffect(() => {
|
|
||||||
const loadCredentials = async () => {
|
|
||||||
try {
|
|
||||||
const token = await SecureStore.getItemAsync('accessToken');
|
|
||||||
const user = await SecureStore.getItemAsync('userName');
|
|
||||||
const uid = await SecureStore.getItemAsync('userId');
|
|
||||||
setAuthToken(token);
|
|
||||||
setUserName(user);
|
|
||||||
setUserId(uid);
|
|
||||||
console.log('Loaded credentials for WebView:', { hasToken: !!token, user, uid });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load credentials:', err);
|
|
||||||
} finally {
|
|
||||||
setIsTokenLoaded(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadCredentials();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// JavaScript to inject token into localStorage before page loads
|
|
||||||
// Web app uses auth2 key with JSON object: {username, token, user_id}
|
|
||||||
const injectedJavaScript = authToken
|
|
||||||
? `
|
|
||||||
(function() {
|
|
||||||
try {
|
|
||||||
// Web app expects auth2 as JSON object with these exact fields
|
|
||||||
var authData = {
|
|
||||||
username: '${userName || ''}',
|
|
||||||
token: '${authToken}',
|
|
||||||
user_id: ${userId || 'null'}
|
|
||||||
};
|
|
||||||
localStorage.setItem('auth2', JSON.stringify(authData));
|
|
||||||
console.log('Auth data injected:', authData.username, 'user_id:', authData.user_id);
|
|
||||||
} catch(e) {
|
|
||||||
console.error('Failed to inject token:', e);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
true;
|
|
||||||
`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
|
||||||
setError(null);
|
|
||||||
setIsLoading(true);
|
|
||||||
webViewRef.current?.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWebViewBack = () => {
|
|
||||||
if (canGoBack) {
|
|
||||||
webViewRef.current?.goBack();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNavigationStateChange = (navState: any) => {
|
|
||||||
setCanGoBack(navState.canGoBack);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleError = () => {
|
|
||||||
setError('Failed to load dashboard. Please check your internet connection.');
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoBack = () => {
|
|
||||||
router.back();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wait for beneficiary data and token to load
|
|
||||||
if (isBeneficiaryLoading || (!isTokenLoaded && !isLocal)) {
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
|
||||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
|
||||||
<View style={styles.placeholder} />
|
|
||||||
</View>
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
||||||
<Text style={styles.loadingText}>Preparing dashboard...</Text>
|
|
||||||
</View>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NO SUBSCRIPTION - Show payment screen with Stripe integration
|
|
||||||
if (!hasActiveSubscription && beneficiary) {
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
|
||||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
|
||||||
<View style={styles.placeholder} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<SubscriptionPayment
|
|
||||||
beneficiary={beneficiary}
|
|
||||||
onSuccess={() => loadBeneficiary()}
|
|
||||||
/>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
|
||||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
|
||||||
<View style={styles.placeholder} />
|
|
||||||
</View>
|
|
||||||
<FullScreenError message={error} onRetry={handleRefresh} />
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
|
||||||
{/* Header */}
|
|
||||||
<View style={styles.header}>
|
|
||||||
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
|
||||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View style={styles.headerCenter}>
|
|
||||||
{currentBeneficiary && (
|
|
||||||
currentBeneficiary.avatar ? (
|
|
||||||
<Image
|
|
||||||
source={{ uri: currentBeneficiary.avatar }}
|
|
||||||
style={styles.avatarSmallImage}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<View style={styles.avatarSmall}>
|
|
||||||
<Text style={styles.avatarText}>
|
|
||||||
{currentBeneficiary.name.charAt(0).toUpperCase()}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<View>
|
|
||||||
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
|
||||||
{currentBeneficiary?.relationship && (
|
|
||||||
<Text style={styles.headerSubtitle}>{currentBeneficiary.relationship}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.headerActions}>
|
|
||||||
{/* WebView navigation only for real beneficiaries */}
|
|
||||||
{!isLocal && canGoBack && (
|
|
||||||
<TouchableOpacity style={styles.actionButton} onPress={handleWebViewBack}>
|
|
||||||
<Ionicons name="chevron-back" size={22} color={AppColors.primary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{!isLocal && (
|
|
||||||
<TouchableOpacity style={styles.actionButton} onPress={handleRefresh}>
|
|
||||||
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Menu button */}
|
|
||||||
<TouchableOpacity style={styles.menuButton} onPress={() => setIsMenuVisible(!isMenuVisible)}>
|
|
||||||
<Ionicons name="ellipsis-vertical" size={22} color={AppColors.textPrimary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
|
||||||
{isMenuVisible && (
|
|
||||||
<View style={styles.dropdownMenu}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.dropdownItem}
|
|
||||||
onPress={() => {
|
|
||||||
setIsMenuVisible(false);
|
|
||||||
handleEditPress();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name="create-outline" size={20} color={AppColors.textPrimary} />
|
|
||||||
<Text style={styles.dropdownItemText}>Edit</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.dropdownItem}
|
|
||||||
onPress={() => {
|
|
||||||
setIsMenuVisible(false);
|
|
||||||
router.push(`/(tabs)/beneficiaries/${id}/share`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name="people-outline" size={20} color={AppColors.textPrimary} />
|
|
||||||
<Text style={styles.dropdownItemText}>Access</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.dropdownItem}
|
|
||||||
onPress={() => {
|
|
||||||
setIsMenuVisible(false);
|
|
||||||
router.push(`/(tabs)/beneficiaries/${id}/equipment`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name="hardware-chip-outline" size={20} color={AppColors.textPrimary} />
|
|
||||||
<Text style={styles.dropdownItemText}>Equipment</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Backdrop to close menu */}
|
|
||||||
{isMenuVisible && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.menuBackdrop}
|
|
||||||
activeOpacity={1}
|
|
||||||
onPress={() => setIsMenuVisible(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Dashboard Content - Mock for local, WebView for real */}
|
|
||||||
{isLocal ? (
|
|
||||||
<MockDashboard beneficiaryName={beneficiaryName} />
|
|
||||||
) : (
|
|
||||||
<View style={styles.webViewContainer}>
|
|
||||||
<WebView
|
|
||||||
ref={webViewRef}
|
|
||||||
source={{ uri: dashboardUrl }}
|
|
||||||
style={styles.webView}
|
|
||||||
onLoadStart={() => setIsLoading(true)}
|
|
||||||
onLoadEnd={() => setIsLoading(false)}
|
|
||||||
onError={handleError}
|
|
||||||
onHttpError={handleError}
|
|
||||||
onNavigationStateChange={handleNavigationStateChange}
|
|
||||||
javaScriptEnabled={true}
|
|
||||||
domStorageEnabled={true}
|
|
||||||
startInLoadingState={true}
|
|
||||||
scalesPageToFit={true}
|
|
||||||
allowsBackForwardNavigationGestures={true}
|
|
||||||
// Inject token into localStorage BEFORE content loads
|
|
||||||
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
|
||||||
// Also inject after load in case page reads localStorage late
|
|
||||||
injectedJavaScript={injectedJavaScript}
|
|
||||||
renderLoading={() => (
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
||||||
<Text style={styles.loadingText}>Loading dashboard...</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<View style={styles.loadingOverlay}>
|
|
||||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Edit Modal */}
|
|
||||||
<Modal
|
|
||||||
visible={isEditModalVisible}
|
|
||||||
transparent
|
|
||||||
animationType="none"
|
|
||||||
onRequestClose={() => setIsEditModalVisible(false)}
|
|
||||||
>
|
|
||||||
<KeyboardAvoidingView
|
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
||||||
style={styles.modalOverlay}
|
|
||||||
>
|
|
||||||
<Animated.View style={[styles.modalOverlay, { opacity: fadeAnim }]}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.modalBackdrop}
|
|
||||||
activeOpacity={1}
|
|
||||||
onPress={() => setIsEditModalVisible(false)}
|
|
||||||
/>
|
|
||||||
<View style={styles.modalContent}>
|
|
||||||
<View style={styles.modalHeader}>
|
|
||||||
<Text style={styles.modalTitle}>Edit Profile</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.modalCloseButton}
|
|
||||||
onPress={() => setIsEditModalVisible(false)}
|
|
||||||
>
|
|
||||||
<Ionicons name="close" size={24} color={AppColors.textSecondary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView showsVerticalScrollIndicator={false}>
|
|
||||||
{/* Avatar Picker */}
|
|
||||||
<TouchableOpacity style={styles.avatarPicker} onPress={handlePickAvatar}>
|
|
||||||
{editForm.avatar ? (
|
|
||||||
<Image source={{ uri: editForm.avatar }} style={styles.avatarPickerImage} />
|
|
||||||
) : (
|
|
||||||
<View style={styles.avatarPickerPlaceholder}>
|
|
||||||
<Text style={styles.avatarPickerLetter}>
|
|
||||||
{editForm.name.charAt(0).toUpperCase() || '?'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={styles.avatarPickerBadge}>
|
|
||||||
<Ionicons name="camera" size={16} color={AppColors.white} />
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* Name Field */}
|
|
||||||
<View style={styles.inputGroup}>
|
|
||||||
<Text style={styles.inputLabel}>Name *</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.textInput}
|
|
||||||
value={editForm.name}
|
|
||||||
onChangeText={(text) => setEditForm(prev => ({ ...prev, name: text }))}
|
|
||||||
placeholder="Enter name"
|
|
||||||
placeholderTextColor={AppColors.textMuted}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Address Field */}
|
|
||||||
<View style={styles.inputGroup}>
|
|
||||||
<Text style={styles.inputLabel}>Address</Text>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.textInput, styles.textInputMultiline]}
|
|
||||||
value={editForm.address}
|
|
||||||
onChangeText={(text) => setEditForm(prev => ({ ...prev, address: text }))}
|
|
||||||
placeholder="Enter address (optional)"
|
|
||||||
placeholderTextColor={AppColors.textMuted}
|
|
||||||
multiline
|
|
||||||
numberOfLines={2}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<View style={styles.modalActions}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.cancelButton}
|
|
||||||
onPress={() => setIsEditModalVisible(false)}
|
|
||||||
>
|
|
||||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.saveButton}
|
|
||||||
onPress={handleSaveEdit}
|
|
||||||
>
|
|
||||||
<Text style={styles.saveButtonText}>Save</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
</KeyboardAvoidingView>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: Spacing.md,
|
|
||||||
paddingVertical: Spacing.sm,
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: AppColors.border,
|
|
||||||
zIndex: 1001,
|
|
||||||
},
|
|
||||||
backButton: {
|
|
||||||
padding: Spacing.xs,
|
|
||||||
},
|
|
||||||
headerCenter: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginLeft: Spacing.sm,
|
|
||||||
},
|
|
||||||
avatarSmall: {
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
borderRadius: BorderRadius.full,
|
|
||||||
backgroundColor: AppColors.primaryLight,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: Spacing.sm,
|
|
||||||
},
|
|
||||||
avatarSmallImage: {
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
borderRadius: 18,
|
|
||||||
marginRight: Spacing.sm,
|
|
||||||
},
|
|
||||||
avatarText: {
|
|
||||||
fontSize: FontSizes.base,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: AppColors.white,
|
|
||||||
},
|
|
||||||
headerTitle: {
|
|
||||||
fontSize: FontSizes.lg,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
},
|
|
||||||
headerSubtitle: {
|
|
||||||
fontSize: FontSizes.xs,
|
|
||||||
color: AppColors.textSecondary,
|
|
||||||
},
|
|
||||||
headerActions: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
actionButton: {
|
|
||||||
padding: Spacing.xs,
|
|
||||||
marginLeft: Spacing.xs,
|
|
||||||
},
|
|
||||||
menuButton: {
|
|
||||||
padding: Spacing.xs,
|
|
||||||
marginLeft: Spacing.sm,
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
width: 32,
|
|
||||||
},
|
|
||||||
// Dropdown Menu
|
|
||||||
dropdownMenu: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 40,
|
|
||||||
right: 0,
|
|
||||||
backgroundColor: AppColors.surface,
|
|
||||||
borderRadius: BorderRadius.lg,
|
|
||||||
minWidth: 160,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.15,
|
|
||||||
shadowRadius: 12,
|
|
||||||
elevation: 8,
|
|
||||||
zIndex: 1000,
|
|
||||||
},
|
|
||||||
dropdownItem: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingVertical: Spacing.md,
|
|
||||||
paddingHorizontal: Spacing.lg,
|
|
||||||
gap: Spacing.md,
|
|
||||||
},
|
|
||||||
dropdownItemText: {
|
|
||||||
fontSize: FontSizes.base,
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
},
|
|
||||||
menuBackdrop: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
zIndex: 999,
|
|
||||||
},
|
|
||||||
webViewContainer: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
webView: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
loadingContainer: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
},
|
|
||||||
loadingOverlay: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.8)',
|
|
||||||
},
|
|
||||||
loadingText: {
|
|
||||||
marginTop: Spacing.md,
|
|
||||||
fontSize: FontSizes.base,
|
|
||||||
color: AppColors.textSecondary,
|
|
||||||
},
|
|
||||||
// No Subscription Styles
|
|
||||||
noSubscriptionContainer: {
|
|
||||||
flex: 1,
|
|
||||||
padding: Spacing.xl,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
noSubIconContainer: {
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
borderRadius: 50,
|
|
||||||
backgroundColor: AppColors.accentLight,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: Spacing.lg,
|
|
||||||
},
|
|
||||||
noSubTitle: {
|
|
||||||
fontSize: FontSizes['2xl'],
|
|
||||||
fontWeight: FontWeights.bold,
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: Spacing.sm,
|
|
||||||
},
|
|
||||||
noSubSubtitle: {
|
|
||||||
fontSize: FontSizes.base,
|
|
||||||
color: AppColors.textSecondary,
|
|
||||||
textAlign: 'center',
|
|
||||||
lineHeight: 24,
|
|
||||||
marginBottom: Spacing.xl,
|
|
||||||
paddingHorizontal: Spacing.md,
|
|
||||||
},
|
|
||||||
noSubPriceCard: {
|
|
||||||
width: '100%',
|
|
||||||
backgroundColor: AppColors.surface,
|
|
||||||
borderRadius: BorderRadius.xl,
|
|
||||||
padding: Spacing.lg,
|
|
||||||
marginBottom: Spacing.xl,
|
|
||||||
...Shadows.sm,
|
|
||||||
},
|
|
||||||
noSubPriceHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: Spacing.md,
|
|
||||||
},
|
|
||||||
noSubPlanName: {
|
|
||||||
fontSize: FontSizes.lg,
|
|
||||||
fontWeight: FontWeights.semibold,
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
},
|
|
||||||
noSubPlanDesc: {
|
|
||||||
fontSize: FontSizes.sm,
|
|
||||||
color: AppColors.textSecondary,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
noSubPriceBadge: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'baseline',
|
|
||||||
},
|
|
||||||
noSubPriceAmount: {
|
|
||||||
fontSize: FontSizes['2xl'],
|
|
||||||
fontWeight: FontWeights.bold,
|
|
||||||
color: AppColors.primary,
|
|
||||||
},
|
|
||||||
noSubPriceUnit: {
|
|
||||||
fontSize: FontSizes.sm,
|
|
||||||
color: AppColors.textMuted,
|
|
||||||
marginLeft: 2,
|
|
||||||
},
|
|
||||||
noSubFeatures: {
|
|
||||||
gap: Spacing.sm,
|
|
||||||
},
|
|
||||||
noSubFeatureRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: Spacing.sm,
|
|
||||||
},
|
|
||||||
noSubFeatureText: {
|
|
||||||
fontSize: FontSizes.sm,
|
|
||||||
color: AppColors.textSecondary,
|
|
||||||
},
|
|
||||||
// Edit Modal Styles
|
|
||||||
modalOverlay: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
},
|
|
||||||
modalBackdrop: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
||||||
},
|
|
||||||
modalContent: {
|
|
||||||
backgroundColor: AppColors.surface,
|
|
||||||
borderTopLeftRadius: BorderRadius['2xl'],
|
|
||||||
borderTopRightRadius: BorderRadius['2xl'],
|
|
||||||
padding: Spacing.lg,
|
|
||||||
maxHeight: '80%',
|
|
||||||
},
|
|
||||||
modalHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: Spacing.lg,
|
|
||||||
},
|
|
||||||
modalTitle: {
|
|
||||||
fontSize: FontSizes.xl,
|
|
||||||
fontWeight: FontWeights.bold,
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
},
|
|
||||||
modalCloseButton: {
|
|
||||||
padding: Spacing.xs,
|
|
||||||
},
|
|
||||||
avatarPicker: {
|
|
||||||
alignSelf: 'center',
|
|
||||||
marginBottom: Spacing.xl,
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
avatarPickerImage: {
|
|
||||||
width: AvatarSizes.xl,
|
|
||||||
height: AvatarSizes.xl,
|
|
||||||
borderRadius: AvatarSizes.xl / 2,
|
|
||||||
},
|
|
||||||
avatarPickerPlaceholder: {
|
|
||||||
width: AvatarSizes.xl,
|
|
||||||
height: AvatarSizes.xl,
|
|
||||||
borderRadius: AvatarSizes.xl / 2,
|
|
||||||
backgroundColor: AppColors.primaryLighter,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
avatarPickerLetter: {
|
|
||||||
fontSize: FontSizes['3xl'],
|
|
||||||
fontWeight: FontWeights.bold,
|
|
||||||
color: AppColors.primary,
|
|
||||||
},
|
|
||||||
avatarPickerBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: 16,
|
|
||||||
backgroundColor: AppColors.primary,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderWidth: 3,
|
|
||||||
borderColor: AppColors.surface,
|
|
||||||
},
|
|
||||||
inputGroup: {
|
|
||||||
marginBottom: Spacing.lg,
|
|
||||||
},
|
|
||||||
inputLabel: {
|
|
||||||
fontSize: FontSizes.sm,
|
|
||||||
fontWeight: FontWeights.medium,
|
|
||||||
color: AppColors.textSecondary,
|
|
||||||
marginBottom: Spacing.sm,
|
|
||||||
},
|
|
||||||
textInput: {
|
|
||||||
backgroundColor: AppColors.background,
|
|
||||||
borderRadius: BorderRadius.lg,
|
|
||||||
padding: Spacing.md,
|
|
||||||
fontSize: FontSizes.base,
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: AppColors.border,
|
|
||||||
},
|
|
||||||
textInputMultiline: {
|
|
||||||
minHeight: 80,
|
|
||||||
textAlignVertical: 'top',
|
|
||||||
},
|
|
||||||
modalActions: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: Spacing.md,
|
|
||||||
marginTop: Spacing.lg,
|
|
||||||
},
|
|
||||||
cancelButton: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: AppColors.surfaceSecondary,
|
|
||||||
paddingVertical: Spacing.md,
|
|
||||||
borderRadius: BorderRadius.lg,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
cancelButtonText: {
|
|
||||||
fontSize: FontSizes.base,
|
|
||||||
fontWeight: FontWeights.semibold,
|
|
||||||
color: AppColors.textSecondary,
|
|
||||||
},
|
|
||||||
saveButton: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: AppColors.primary,
|
|
||||||
paddingVertical: Spacing.md,
|
|
||||||
borderRadius: BorderRadius.lg,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
saveButtonText: {
|
|
||||||
fontSize: FontSizes.base,
|
|
||||||
fontWeight: FontWeights.semibold,
|
|
||||||
color: AppColors.white,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@ -14,7 +14,10 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
Animated,
|
Animated,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Switch,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { WebView } from 'react-native-webview';
|
||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
import { useLocalSearchParams, router } from 'expo-router';
|
import { useLocalSearchParams, router } from 'expo-router';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
@ -49,64 +52,178 @@ const isLocalBeneficiary = (id: string | number): boolean => {
|
|||||||
// Setup state types
|
// Setup state types
|
||||||
type SetupState = 'loading' | 'awaiting_equipment' | 'no_devices' | 'no_subscription' | 'ready';
|
type SetupState = 'loading' | 'awaiting_equipment' | 'no_devices' | 'no_subscription' | 'ready';
|
||||||
|
|
||||||
|
// Stripe API
|
||||||
|
const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
|
||||||
|
|
||||||
|
// WebView Dashboard URL - uses test NDK account for demo data
|
||||||
|
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
|
||||||
|
const TEST_NDK_DEPLOYMENT_ID = '1'; // anandk test deployment with real sensor data
|
||||||
|
|
||||||
// Starter Kit info
|
// Starter Kit info
|
||||||
const STARTER_KIT = {
|
const STARTER_KIT = {
|
||||||
name: 'WellNuo Starter Kit',
|
name: 'WellNuo Starter Kit',
|
||||||
price: '$249',
|
price: '$249',
|
||||||
|
priceValue: 249,
|
||||||
features: [
|
features: [
|
||||||
'Motion sensor (PIR)',
|
'Motion sensor (PIR)',
|
||||||
'Door/window sensor',
|
'Door/window sensor',
|
||||||
'Temperature & humidity sensor',
|
'Temperature & humidity sensor',
|
||||||
'WellNuo Hub',
|
'WellNuo Hub',
|
||||||
|
'Mobile app access',
|
||||||
|
'1 year subscription included',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// No Devices Screen Component - Primary: Buy Kit, Secondary: I have sensors
|
// No Devices Screen Component - Primary: Buy Kit with Stripe, Secondary: I have sensors
|
||||||
function NoDevicesScreen({
|
function NoDevicesScreen({
|
||||||
beneficiary,
|
beneficiary,
|
||||||
|
beneficiaryId,
|
||||||
onActivate,
|
onActivate,
|
||||||
onGetSensors
|
onPurchaseSuccess,
|
||||||
|
userEmail,
|
||||||
|
userId,
|
||||||
}: {
|
}: {
|
||||||
beneficiary: Beneficiary;
|
beneficiary: Beneficiary;
|
||||||
|
beneficiaryId: string;
|
||||||
onActivate: () => void;
|
onActivate: () => void;
|
||||||
onGetSensors: () => void;
|
onPurchaseSuccess: () => void;
|
||||||
|
userEmail?: string;
|
||||||
|
userId?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const handlePurchase = async () => {
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Create Payment Sheet on server
|
||||||
|
const response = await fetch(`${STRIPE_API_URL}/create-payment-sheet`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: userEmail || 'guest@wellnuo.com',
|
||||||
|
amount: STARTER_KIT.priceValue * 100, // Convert to cents ($249.00)
|
||||||
|
metadata: {
|
||||||
|
userId: userId || 'guest',
|
||||||
|
beneficiaryName: beneficiary.name,
|
||||||
|
beneficiaryId: beneficiaryId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.paymentIntent) {
|
||||||
|
throw new Error(data.error || 'Failed to create payment sheet');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Initialize the Payment Sheet
|
||||||
|
const { error: initError } = await initPaymentSheet({
|
||||||
|
merchantDisplayName: 'WellNuo',
|
||||||
|
paymentIntentClientSecret: data.paymentIntent,
|
||||||
|
customerId: data.customer,
|
||||||
|
customerEphemeralKeySecret: data.ephemeralKey,
|
||||||
|
defaultBillingDetails: {
|
||||||
|
email: userEmail || '',
|
||||||
|
},
|
||||||
|
returnURL: 'wellnuo://stripe-redirect',
|
||||||
|
applePay: {
|
||||||
|
merchantCountryCode: 'US',
|
||||||
|
},
|
||||||
|
googlePay: {
|
||||||
|
merchantCountryCode: 'US',
|
||||||
|
testEnv: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initError) {
|
||||||
|
throw new Error(initError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Present the Payment Sheet
|
||||||
|
const { error: presentError } = await presentPaymentSheet();
|
||||||
|
|
||||||
|
if (presentError) {
|
||||||
|
if (presentError.code === 'Canceled') {
|
||||||
|
// User cancelled - do nothing
|
||||||
|
setIsProcessing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(presentError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Payment successful!
|
||||||
|
toast.success('Order Placed!', 'Your WellNuo Starter Kit is on its way.');
|
||||||
|
onPurchaseSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Payment error:', error);
|
||||||
|
toast.error(
|
||||||
|
'Payment Failed',
|
||||||
|
error instanceof Error ? error.message : 'Something went wrong. Please try again.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.setupContainer}>
|
<ScrollView contentContainerStyle={styles.setupScrollContent}>
|
||||||
<View style={styles.setupIconContainer}>
|
<View style={styles.setupContainer}>
|
||||||
<Ionicons name="hardware-chip-outline" size={48} color={AppColors.primary} />
|
<View style={styles.setupIconContainer}>
|
||||||
</View>
|
<Ionicons name="hardware-chip-outline" size={48} color={AppColors.primary} />
|
||||||
<Text style={styles.setupTitle}>Get Started with WellNuo</Text>
|
</View>
|
||||||
<Text style={styles.setupSubtitle}>
|
<Text style={styles.setupTitle}>Get Started with WellNuo</Text>
|
||||||
To start monitoring {beneficiary.name}'s wellness, you need WellNuo sensors.
|
<Text style={styles.setupSubtitle}>
|
||||||
</Text>
|
To start monitoring {beneficiary.name}'s wellness, you need WellNuo sensors.
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* Primary: Buy Kit Card */}
|
{/* Primary: Buy Kit Card */}
|
||||||
<View style={styles.buyKitCard}>
|
<View style={styles.buyKitCard}>
|
||||||
<Text style={styles.buyKitName}>{STARTER_KIT.name}</Text>
|
<Text style={styles.buyKitName}>{STARTER_KIT.name}</Text>
|
||||||
<Text style={styles.buyKitPrice}>{STARTER_KIT.price}</Text>
|
<Text style={styles.buyKitPrice}>{STARTER_KIT.price}</Text>
|
||||||
|
|
||||||
<View style={styles.buyKitFeatures}>
|
<View style={styles.buyKitFeatures}>
|
||||||
{STARTER_KIT.features.map((feature, index) => (
|
{STARTER_KIT.features.map((feature, index) => (
|
||||||
<View key={index} style={styles.buyKitFeatureRow}>
|
<View key={index} style={styles.buyKitFeatureRow}>
|
||||||
<Ionicons name="checkmark-circle" size={18} color={AppColors.success} />
|
<Ionicons name="checkmark-circle" size={18} color={AppColors.success} />
|
||||||
<Text style={styles.buyKitFeatureText}>{feature}</Text>
|
<Text style={styles.buyKitFeatureText}>{feature}</Text>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.buyKitButton, isProcessing && styles.buyKitButtonDisabled]}
|
||||||
|
onPress={handlePurchase}
|
||||||
|
disabled={isProcessing}
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<ActivityIndicator color={AppColors.white} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Ionicons name="card" size={20} color={AppColors.white} />
|
||||||
|
<Text style={styles.buyKitButtonText}>Buy Now - {STARTER_KIT.price}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Security Badge */}
|
||||||
|
<View style={styles.securityBadge}>
|
||||||
|
<Ionicons name="shield-checkmark" size={16} color={AppColors.success} />
|
||||||
|
<Text style={styles.securityText}>Secure payment by Stripe</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.buyKitButton} onPress={onGetSensors}>
|
{/* Secondary: I already have sensors */}
|
||||||
<Ionicons name="cart" size={20} color={AppColors.white} />
|
<TouchableOpacity style={styles.alreadyHaveLink} onPress={onActivate}>
|
||||||
<Text style={styles.buyKitButtonText}>Buy Now - {STARTER_KIT.price}</Text>
|
<Text style={styles.alreadyHaveLinkText}>I already have sensors</Text>
|
||||||
|
<Ionicons name="arrow-forward" size={16} color={AppColors.textSecondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
</ScrollView>
|
||||||
{/* Secondary: I already have sensors */}
|
|
||||||
<TouchableOpacity style={styles.alreadyHaveLink} onPress={onActivate}>
|
|
||||||
<Text style={styles.alreadyHaveLinkText}>I already have sensors</Text>
|
|
||||||
<Ionicons name="arrow-forward" size={16} color={AppColors.textSecondary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,12 +358,20 @@ function AwaitingEquipmentScreen({
|
|||||||
export default function BeneficiaryDetailScreen() {
|
export default function BeneficiaryDetailScreen() {
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const { setCurrentBeneficiary, localBeneficiaries, updateLocalBeneficiary, removeLocalBeneficiary } = useBeneficiary();
|
const { setCurrentBeneficiary, localBeneficiaries, updateLocalBeneficiary, removeLocalBeneficiary } = useBeneficiary();
|
||||||
|
const { user } = useAuth();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
|
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Developer toggle for WebView
|
||||||
|
const [showWebView, setShowWebView] = useState(false);
|
||||||
|
const [authToken, setAuthToken] = useState<string | null>(null);
|
||||||
|
const [userName, setUserName] = useState<string | null>(null);
|
||||||
|
const [userId, setUserId] = useState<string | null>(null);
|
||||||
|
const webViewRef = useRef<WebView>(null);
|
||||||
|
|
||||||
// Check if this is a local beneficiary
|
// Check if this is a local beneficiary
|
||||||
const isLocal = useMemo(() => id ? isLocalBeneficiary(id) : false, [id]);
|
const isLocal = useMemo(() => id ? isLocalBeneficiary(id) : false, [id]);
|
||||||
|
|
||||||
@ -256,17 +381,25 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
if (isLoading) return 'loading';
|
if (isLoading) return 'loading';
|
||||||
if (!beneficiary) return 'loading';
|
if (!beneficiary) return 'loading';
|
||||||
|
|
||||||
// Check if awaiting equipment (ordered, shipped, or delivered but not activated)
|
// Check if has devices - used in multiple places
|
||||||
const equipmentStatus = beneficiary.equipmentStatus;
|
|
||||||
if (equipmentStatus && ['ordered', 'shipped', 'delivered'].includes(equipmentStatus)) {
|
|
||||||
return 'awaiting_equipment';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if has devices - required first step
|
|
||||||
const hasDevices = beneficiary.hasDevices ||
|
const hasDevices = beneficiary.hasDevices ||
|
||||||
(beneficiary.devices && beneficiary.devices.length > 0) ||
|
(beneficiary.devices && beneficiary.devices.length > 0) ||
|
||||||
beneficiary.device_id;
|
beneficiary.device_id;
|
||||||
|
|
||||||
|
// Check equipment status
|
||||||
|
const equipmentStatus = beneficiary.equipmentStatus;
|
||||||
|
|
||||||
|
// If equipment is ordered/shipped/delivered but NOT yet activated (no devices)
|
||||||
|
// show awaiting equipment screen
|
||||||
|
if (equipmentStatus && ['ordered', 'shipped', 'delivered'].includes(equipmentStatus)) {
|
||||||
|
// But if user already has devices (activated), skip to next step
|
||||||
|
if (!hasDevices) {
|
||||||
|
return 'awaiting_equipment';
|
||||||
|
}
|
||||||
|
// Has devices = already activated, continue to subscription check
|
||||||
|
}
|
||||||
|
|
||||||
|
// No devices and no equipment ordered = show purchase screen
|
||||||
if (!hasDevices) return 'no_devices';
|
if (!hasDevices) return 'no_devices';
|
||||||
|
|
||||||
// Check subscription - required after devices connected
|
// Check subscription - required after devices connected
|
||||||
@ -339,6 +472,23 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
loadBeneficiary();
|
loadBeneficiary();
|
||||||
}, [loadBeneficiary]);
|
}, [loadBeneficiary]);
|
||||||
|
|
||||||
|
// Load credentials for WebView
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCredentials = async () => {
|
||||||
|
try {
|
||||||
|
const token = await SecureStore.getItemAsync('accessToken');
|
||||||
|
const user = await SecureStore.getItemAsync('userName');
|
||||||
|
const uid = await SecureStore.getItemAsync('userId');
|
||||||
|
setAuthToken(token);
|
||||||
|
setUserName(user);
|
||||||
|
setUserId(uid);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load credentials:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadCredentials();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Sync beneficiary data when localBeneficiaries changes (especially after avatar update)
|
// Sync beneficiary data when localBeneficiaries changes (especially after avatar update)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLocal && id && beneficiary) {
|
if (isLocal && id && beneficiary) {
|
||||||
@ -361,12 +511,26 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGetSensors = () => {
|
const handlePurchaseSuccess = async () => {
|
||||||
// Navigate to purchase screen with beneficiary info
|
// Update beneficiary with ordered status and subscription (kit includes 1 year)
|
||||||
router.push({
|
if (id && isLocal) {
|
||||||
pathname: '/(auth)/purchase',
|
// Calculate subscription end date (1 year from now)
|
||||||
params: { beneficiaryId: id!, lovedOneName: beneficiary?.name },
|
const subscriptionEnd = new Date();
|
||||||
});
|
subscriptionEnd.setFullYear(subscriptionEnd.getFullYear() + 1);
|
||||||
|
|
||||||
|
await updateLocalBeneficiary(parseInt(id, 10), {
|
||||||
|
equipmentStatus: 'ordered',
|
||||||
|
// Kit includes 1 year subscription
|
||||||
|
subscription: {
|
||||||
|
status: 'pending', // Will activate when sensors are connected
|
||||||
|
plan: 'yearly',
|
||||||
|
startDate: new Date().toISOString(),
|
||||||
|
endDate: subscriptionEnd.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Reload beneficiary to show new state
|
||||||
|
loadBeneficiary(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkReceived = async () => {
|
const handleMarkReceived = async () => {
|
||||||
@ -454,6 +618,26 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
toast.info('Coming Soon', `${featureName} is currently in development.`);
|
toast.info('Coming Soon', `${featureName} is currently in development.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// JavaScript to inject token into localStorage for WebView
|
||||||
|
const injectedJavaScript = authToken
|
||||||
|
? `
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var authData = {
|
||||||
|
username: '${userName || ''}',
|
||||||
|
token: '${authToken}',
|
||||||
|
user_id: ${userId || 'null'}
|
||||||
|
};
|
||||||
|
localStorage.setItem('auth2', JSON.stringify(authData));
|
||||||
|
console.log('Auth data injected:', authData.username);
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Failed to inject token:', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
true;
|
||||||
|
`
|
||||||
|
: '';
|
||||||
|
|
||||||
const handleDeleteBeneficiary = () => {
|
const handleDeleteBeneficiary = () => {
|
||||||
if (!isLocal || !id) return;
|
if (!isLocal || !id) return;
|
||||||
|
|
||||||
@ -509,8 +693,11 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
return (
|
return (
|
||||||
<NoDevicesScreen
|
<NoDevicesScreen
|
||||||
beneficiary={beneficiary}
|
beneficiary={beneficiary}
|
||||||
|
beneficiaryId={id!}
|
||||||
onActivate={handleActivateSensors}
|
onActivate={handleActivateSensors}
|
||||||
onGetSensors={handleGetSensors}
|
onPurchaseSuccess={handlePurchaseSuccess}
|
||||||
|
userEmail={user?.email}
|
||||||
|
userId={user?.user_id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -524,6 +711,51 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
|
|
||||||
case 'ready':
|
case 'ready':
|
||||||
default:
|
default:
|
||||||
|
// WebView mode - uses test NDK deployment for demo data
|
||||||
|
if (showWebView) {
|
||||||
|
const webViewUrl = `${DASHBOARD_URL}?deployment_id=${TEST_NDK_DEPLOYMENT_ID}`;
|
||||||
|
return (
|
||||||
|
<View style={styles.webViewContainer}>
|
||||||
|
<WebView
|
||||||
|
ref={webViewRef}
|
||||||
|
source={{ uri: webViewUrl }}
|
||||||
|
style={styles.webView}
|
||||||
|
javaScriptEnabled={true}
|
||||||
|
domStorageEnabled={true}
|
||||||
|
startInLoadingState={true}
|
||||||
|
allowsBackForwardNavigationGestures={true}
|
||||||
|
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
||||||
|
injectedJavaScript={injectedJavaScript}
|
||||||
|
renderLoading={() => (
|
||||||
|
<View style={styles.webViewLoading}>
|
||||||
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||||
|
<Text style={styles.webViewLoadingText}>Loading dashboard...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* Developer Toggle - always visible to switch back */}
|
||||||
|
<View style={styles.webViewToggleOverlay}>
|
||||||
|
<View style={styles.devToggleCard}>
|
||||||
|
<View style={styles.devToggleLeft}>
|
||||||
|
<Ionicons name="code-slash" size={20} color={AppColors.warning} />
|
||||||
|
<View>
|
||||||
|
<Text style={styles.devToggleLabel}>Developer Mode</Text>
|
||||||
|
<Text style={styles.devToggleHint}>Show WebView dashboard</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={showWebView}
|
||||||
|
onValueChange={setShowWebView}
|
||||||
|
trackColor={{ false: AppColors.border, true: AppColors.primaryLight }}
|
||||||
|
thumbColor={showWebView ? AppColors.primary : AppColors.textMuted}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native mode
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
@ -537,165 +769,23 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Profile Card */}
|
{/* Developer Toggle for WebView */}
|
||||||
<View style={styles.profileCard}>
|
<View style={styles.devToggleCard}>
|
||||||
<View style={styles.avatarWrapper}>
|
<View style={styles.devToggleLeft}>
|
||||||
{beneficiary.avatar ? (
|
<Ionicons name="code-slash" size={20} color={AppColors.warning} />
|
||||||
<Image source={{ uri: beneficiary.avatar }} style={styles.avatarImage} />
|
<View>
|
||||||
) : (
|
<Text style={styles.devToggleLabel}>Developer Mode</Text>
|
||||||
<View style={styles.avatar}>
|
<Text style={styles.devToggleHint}>Show WebView dashboard</Text>
|
||||||
<Text style={styles.avatarText}>
|
|
||||||
{beneficiary.name.charAt(0).toUpperCase()}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.beneficiaryName}>{beneficiary.name}</Text>
|
|
||||||
|
|
||||||
{beneficiary.address && (
|
|
||||||
<View style={styles.locationRow}>
|
|
||||||
<Ionicons name="location" size={14} color={AppColors.textMuted} />
|
|
||||||
<Text style={styles.locationText}>{beneficiary.address}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={styles.statusBadge}>
|
|
||||||
<View style={[styles.statusIndicator, { backgroundColor: statusColor }]} />
|
|
||||||
<Text style={styles.statusText}>
|
|
||||||
{beneficiary.status === 'online' ? 'Online now' : 'Offline'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{beneficiary.last_activity && (
|
|
||||||
<Text style={styles.lastActivity}>
|
|
||||||
Last activity: {beneficiary.last_activity}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<Text style={styles.sectionTitle}>Quick Actions</Text>
|
|
||||||
<View style={styles.actionsRow}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.actionButton}
|
|
||||||
onPress={() => {
|
|
||||||
setCurrentBeneficiary(beneficiary);
|
|
||||||
router.push('/(tabs)/chat');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={[styles.actionButtonIcon, { backgroundColor: AppColors.accentLight }]}>
|
|
||||||
<Ionicons name="chatbubbles" size={22} color={AppColors.accent} />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.actionButtonText}>Chat with Julia</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.actionButton}
|
|
||||||
onPress={() => router.push(`/(tabs)/beneficiaries/${id}/equipment`)}
|
|
||||||
>
|
|
||||||
<View style={[styles.actionButtonIcon, { backgroundColor: AppColors.primaryLighter }]}>
|
|
||||||
<Ionicons name="hardware-chip" size={22} color={AppColors.primary} />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.actionButtonText}>Equipment</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Subscription Section */}
|
|
||||||
<Text style={styles.sectionTitle}>Subscription</Text>
|
|
||||||
<View style={styles.subscriptionCard}>
|
|
||||||
<View style={styles.subscriptionHeader}>
|
|
||||||
<View style={styles.subscriptionBadgeContainer}>
|
|
||||||
<View style={styles.subscriptionIconBg}>
|
|
||||||
<Ionicons name="diamond" size={18} color={AppColors.accent} />
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<Text style={styles.subscriptionPlan}>WellNuo Pro</Text>
|
|
||||||
<Text style={styles.subscriptionStatus}>Active</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.priceBadge}>
|
|
||||||
<Text style={styles.priceText}>$49</Text>
|
|
||||||
<Text style={styles.priceUnit}>/mo</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<Switch
|
||||||
<View style={styles.subscriptionDivider} />
|
value={showWebView}
|
||||||
|
onValueChange={setShowWebView}
|
||||||
<View style={styles.subscriptionFeatures}>
|
trackColor={{ false: AppColors.border, true: AppColors.primaryLight }}
|
||||||
<View style={styles.featureItem}>
|
thumbColor={showWebView ? AppColors.primary : AppColors.textMuted}
|
||||||
<Ionicons name="checkmark-circle" size={18} color={AppColors.success} />
|
/>
|
||||||
<Text style={styles.featureText}>24/7 AI monitoring</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.featureItem}>
|
|
||||||
<Ionicons name="checkmark-circle" size={18} color={AppColors.success} />
|
|
||||||
<Text style={styles.featureText}>Unlimited chat with Julia</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.featureItem}>
|
|
||||||
<Ionicons name="checkmark-circle" size={18} color={AppColors.success} />
|
|
||||||
<Text style={styles.featureText}>Activity reports</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.manageButton}
|
|
||||||
onPress={() => showComingSoon('Subscription Management')}
|
|
||||||
>
|
|
||||||
<Text style={styles.manageButtonText}>Manage Subscription</Text>
|
|
||||||
<Ionicons name="chevron-forward" size={18} color={AppColors.primary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Settings */}
|
|
||||||
<Text style={styles.sectionTitle}>Settings</Text>
|
|
||||||
<View style={styles.settingsCard}>
|
|
||||||
<TouchableOpacity style={styles.settingsRow} onPress={() => showComingSoon('Notification Settings')}>
|
|
||||||
<View style={styles.settingsRowLeft}>
|
|
||||||
<View style={[styles.settingsIcon, { backgroundColor: AppColors.primarySubtle }]}>
|
|
||||||
<Ionicons name="notifications-outline" size={20} color={AppColors.primary} />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.settingsRowText}>Notifications</Text>
|
|
||||||
</View>
|
|
||||||
<Ionicons name="chevron-forward" size={18} color={AppColors.textMuted} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View style={styles.settingsDivider} />
|
|
||||||
|
|
||||||
<TouchableOpacity style={styles.settingsRow} onPress={() => showComingSoon('Alert Rules')}>
|
|
||||||
<View style={styles.settingsRowLeft}>
|
|
||||||
<View style={[styles.settingsIcon, { backgroundColor: AppColors.warningLight }]}>
|
|
||||||
<Ionicons name="alert-circle-outline" size={20} color={AppColors.warning} />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.settingsRowText}>Alert Rules</Text>
|
|
||||||
</View>
|
|
||||||
<Ionicons name="chevron-forward" size={18} color={AppColors.textMuted} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View style={styles.settingsDivider} />
|
|
||||||
|
|
||||||
<TouchableOpacity style={styles.settingsRow} onPress={() => showComingSoon('Connected Sensors')}>
|
|
||||||
<View style={styles.settingsRowLeft}>
|
|
||||||
<View style={[styles.settingsIcon, { backgroundColor: AppColors.infoLight }]}>
|
|
||||||
<Ionicons name="hardware-chip-outline" size={20} color={AppColors.info} />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.settingsRowText}>Connected Sensors</Text>
|
|
||||||
</View>
|
|
||||||
<Ionicons name="chevron-forward" size={18} color={AppColors.textMuted} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Danger Zone - only for local beneficiaries */}
|
|
||||||
{isLocal && (
|
|
||||||
<>
|
|
||||||
<Text style={[styles.sectionTitle, styles.dangerTitle]}>Danger Zone</Text>
|
|
||||||
<TouchableOpacity style={styles.dangerButton} onPress={handleDeleteBeneficiary}>
|
|
||||||
<Ionicons name="trash-outline" size={20} color={AppColors.error} />
|
|
||||||
<Text style={styles.dangerButtonText}>Remove Beneficiary</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Activity Dashboard */}
|
{/* Activity Dashboard */}
|
||||||
<MockDashboard beneficiaryName={beneficiary.name} />
|
<MockDashboard beneficiaryName={beneficiary.name} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@ -710,7 +800,24 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
<TouchableOpacity style={styles.headerButton} onPress={() => router.back()}>
|
<TouchableOpacity style={styles.headerButton} onPress={() => router.back()}>
|
||||||
<Ionicons name="arrow-back" size={22} color={AppColors.textPrimary} />
|
<Ionicons name="arrow-back" size={22} color={AppColors.textPrimary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
|
|
||||||
|
{/* Avatar + Name */}
|
||||||
|
<View style={styles.headerCenter}>
|
||||||
|
<View style={styles.headerAvatarWrapper}>
|
||||||
|
{beneficiary.avatar ? (
|
||||||
|
<Image source={{ uri: beneficiary.avatar }} style={styles.headerAvatarImage} />
|
||||||
|
) : (
|
||||||
|
<View style={styles.headerAvatar}>
|
||||||
|
<Text style={styles.headerAvatarText}>
|
||||||
|
{beneficiary.name.charAt(0).toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={[styles.headerStatusDot, { backgroundColor: statusColor }]} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View>
|
<View>
|
||||||
<TouchableOpacity style={styles.headerButton} onPress={() => setIsMenuVisible(!isMenuVisible)}>
|
<TouchableOpacity style={styles.headerButton} onPress={() => setIsMenuVisible(!isMenuVisible)}>
|
||||||
<Ionicons name="ellipsis-vertical" size={22} color={AppColors.textPrimary} />
|
<Ionicons name="ellipsis-vertical" size={22} color={AppColors.textPrimary} />
|
||||||
@ -926,6 +1033,98 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: FontWeights.semibold,
|
fontWeight: FontWeights.semibold,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
},
|
},
|
||||||
|
headerCenter: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginHorizontal: Spacing.md,
|
||||||
|
gap: Spacing.sm,
|
||||||
|
},
|
||||||
|
headerAvatarWrapper: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
headerAvatar: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
headerAvatarImage: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
},
|
||||||
|
headerAvatarText: {
|
||||||
|
fontSize: FontSizes.base,
|
||||||
|
fontWeight: FontWeights.bold,
|
||||||
|
color: AppColors.white,
|
||||||
|
},
|
||||||
|
headerStatusDot: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: 6,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: AppColors.background,
|
||||||
|
},
|
||||||
|
// Developer Toggle
|
||||||
|
devToggleCard: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
backgroundColor: AppColors.warningLight,
|
||||||
|
borderRadius: BorderRadius.lg,
|
||||||
|
padding: Spacing.md,
|
||||||
|
marginBottom: Spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: AppColors.warning,
|
||||||
|
},
|
||||||
|
devToggleLeft: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: Spacing.md,
|
||||||
|
},
|
||||||
|
devToggleLabel: {
|
||||||
|
fontSize: FontSizes.sm,
|
||||||
|
fontWeight: FontWeights.semibold,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
},
|
||||||
|
devToggleHint: {
|
||||||
|
fontSize: FontSizes.xs,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
},
|
||||||
|
// WebView
|
||||||
|
webViewContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
webViewToggleOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 100, // Above tab bar
|
||||||
|
left: Spacing.md,
|
||||||
|
right: Spacing.md,
|
||||||
|
},
|
||||||
|
webView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
webViewLoading: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: AppColors.background,
|
||||||
|
},
|
||||||
|
webViewLoadingText: {
|
||||||
|
marginTop: Spacing.md,
|
||||||
|
fontSize: FontSizes.base,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
},
|
||||||
scrollView: {
|
scrollView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@ -934,11 +1133,13 @@ const styles = StyleSheet.create({
|
|||||||
paddingBottom: Spacing.xxl,
|
paddingBottom: Spacing.xxl,
|
||||||
},
|
},
|
||||||
// Setup Screens
|
// Setup Screens
|
||||||
|
setupScrollContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
setupContainer: {
|
setupContainer: {
|
||||||
flex: 1,
|
|
||||||
padding: Spacing.xl,
|
padding: Spacing.xl,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
},
|
||||||
setupIconContainer: {
|
setupIconContainer: {
|
||||||
width: 96,
|
width: 96,
|
||||||
@ -1057,6 +1258,20 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: FontWeights.semibold,
|
fontWeight: FontWeights.semibold,
|
||||||
color: AppColors.white,
|
color: AppColors.white,
|
||||||
},
|
},
|
||||||
|
buyKitButtonDisabled: {
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
securityBadge: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: Spacing.xs,
|
||||||
|
marginTop: Spacing.md,
|
||||||
|
},
|
||||||
|
securityText: {
|
||||||
|
fontSize: FontSizes.xs,
|
||||||
|
color: AppColors.success,
|
||||||
|
},
|
||||||
alreadyHaveLink: {
|
alreadyHaveLink: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@ -10,7 +10,6 @@ export default function BeneficiariesLayout() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="[id]/index" />
|
<Stack.Screen name="[id]/index" />
|
||||||
<Stack.Screen name="[id]/dashboard" />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user