diff --git a/app/(auth)/activate.tsx b/app/(auth)/activate.tsx index 53ed994..3c18f50 100644 --- a/app/(auth)/activate.tsx +++ b/app/(auth)/activate.tsx @@ -60,10 +60,22 @@ export default function ActivateScreen() { try { // If we have an existing beneficiary, update them with device info if (existingBeneficiaryId && existingBeneficiary) { - await updateLocalBeneficiary(existingBeneficiaryId, { + // Prepare update data + const updateData: any = { hasDevices: true, 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); setStep('complete'); } else { diff --git a/app/(tabs)/beneficiaries/[id]/dashboard.tsx b/app/(tabs)/beneficiaries/[id]/dashboard.tsx deleted file mode 100644 index be7e7fc..0000000 --- a/app/(tabs)/beneficiaries/[id]/dashboard.tsx +++ /dev/null @@ -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(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [canGoBack, setCanGoBack] = useState(false); - const [authToken, setAuthToken] = useState(null); - const [userName, setUserName] = useState(null); - const [userId, setUserId] = useState(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(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 ( - - - - - - {beneficiaryName} - - - - - Preparing dashboard... - - - ); - } - - // NO SUBSCRIPTION - Show payment screen with Stripe integration - if (!hasActiveSubscription && beneficiary) { - return ( - - - - - - {beneficiaryName} - - - - loadBeneficiary()} - /> - - ); - } - - if (error) { - return ( - - - - - - {beneficiaryName} - - - - - ); - } - - return ( - - {/* Header */} - - - - - - - {currentBeneficiary && ( - currentBeneficiary.avatar ? ( - - ) : ( - - - {currentBeneficiary.name.charAt(0).toUpperCase()} - - - ) - )} - - {beneficiaryName} - {currentBeneficiary?.relationship && ( - {currentBeneficiary.relationship} - )} - - - - - {/* WebView navigation only for real beneficiaries */} - {!isLocal && canGoBack && ( - - - - )} - {!isLocal && ( - - - - )} - - {/* Menu button */} - setIsMenuVisible(!isMenuVisible)}> - - - - {/* Dropdown Menu */} - {isMenuVisible && ( - - { - setIsMenuVisible(false); - handleEditPress(); - }} - > - - Edit - - - { - setIsMenuVisible(false); - router.push(`/(tabs)/beneficiaries/${id}/share`); - }} - > - - Access - - - { - setIsMenuVisible(false); - router.push(`/(tabs)/beneficiaries/${id}/equipment`); - }} - > - - Equipment - - - )} - - - - {/* Backdrop to close menu */} - {isMenuVisible && ( - setIsMenuVisible(false)} - /> - )} - - {/* Dashboard Content - Mock for local, WebView for real */} - {isLocal ? ( - - ) : ( - - 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={() => ( - - - Loading dashboard... - - )} - /> - - {isLoading && ( - - - - )} - - )} - - {/* Edit Modal */} - setIsEditModalVisible(false)} - > - - - setIsEditModalVisible(false)} - /> - - - Edit Profile - setIsEditModalVisible(false)} - > - - - - - - {/* Avatar Picker */} - - {editForm.avatar ? ( - - ) : ( - - - {editForm.name.charAt(0).toUpperCase() || '?'} - - - )} - - - - - - {/* Name Field */} - - Name * - setEditForm(prev => ({ ...prev, name: text }))} - placeholder="Enter name" - placeholderTextColor={AppColors.textMuted} - /> - - - {/* Address Field */} - - Address - setEditForm(prev => ({ ...prev, address: text }))} - placeholder="Enter address (optional)" - placeholderTextColor={AppColors.textMuted} - multiline - numberOfLines={2} - /> - - - - {/* Action Buttons */} - - setIsEditModalVisible(false)} - > - Cancel - - - Save - - - - - - - - - ); -} - -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, - }, -}); diff --git a/app/(tabs)/beneficiaries/[id]/index.tsx b/app/(tabs)/beneficiaries/[id]/index.tsx index 7070bc5..57e2a0d 100644 --- a/app/(tabs)/beneficiaries/[id]/index.tsx +++ b/app/(tabs)/beneficiaries/[id]/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { View, Text, @@ -14,7 +14,10 @@ import { Platform, Animated, ActivityIndicator, + Switch, } from 'react-native'; +import { WebView } from 'react-native-webview'; +import * as SecureStore from 'expo-secure-store'; import { useLocalSearchParams, router } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -49,64 +52,178 @@ const isLocalBeneficiary = (id: string | number): boolean => { // Setup state types 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 const STARTER_KIT = { name: 'WellNuo Starter Kit', price: '$249', + priceValue: 249, features: [ 'Motion sensor (PIR)', 'Door/window sensor', 'Temperature & humidity sensor', '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({ beneficiary, + beneficiaryId, onActivate, - onGetSensors + onPurchaseSuccess, + userEmail, + userId, }: { beneficiary: Beneficiary; + beneficiaryId: string; 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 ( - - - - - Get Started with WellNuo - - To start monitoring {beneficiary.name}'s wellness, you need WellNuo sensors. - + + + + + + Get Started with WellNuo + + To start monitoring {beneficiary.name}'s wellness, you need WellNuo sensors. + - {/* Primary: Buy Kit Card */} - - {STARTER_KIT.name} - {STARTER_KIT.price} + {/* Primary: Buy Kit Card */} + + {STARTER_KIT.name} + {STARTER_KIT.price} - - {STARTER_KIT.features.map((feature, index) => ( - - - {feature} - - ))} + + {STARTER_KIT.features.map((feature, index) => ( + + + {feature} + + ))} + + + + {isProcessing ? ( + + ) : ( + <> + + Buy Now - {STARTER_KIT.price} + + )} + + + {/* Security Badge */} + + + Secure payment by Stripe + - - - Buy Now - {STARTER_KIT.price} + {/* Secondary: I already have sensors */} + + I already have sensors + - - {/* Secondary: I already have sensors */} - - I already have sensors - - - + ); } @@ -241,12 +358,20 @@ function AwaitingEquipmentScreen({ export default function BeneficiaryDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const { setCurrentBeneficiary, localBeneficiaries, updateLocalBeneficiary, removeLocalBeneficiary } = useBeneficiary(); + const { user } = useAuth(); const toast = useToast(); const [beneficiary, setBeneficiary] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); + // Developer toggle for WebView + const [showWebView, setShowWebView] = useState(false); + const [authToken, setAuthToken] = useState(null); + const [userName, setUserName] = useState(null); + const [userId, setUserId] = useState(null); + const webViewRef = useRef(null); + // Check if this is a local beneficiary const isLocal = useMemo(() => id ? isLocalBeneficiary(id) : false, [id]); @@ -256,17 +381,25 @@ export default function BeneficiaryDetailScreen() { if (isLoading) return 'loading'; if (!beneficiary) return 'loading'; - // Check if awaiting equipment (ordered, shipped, or delivered but not activated) - const equipmentStatus = beneficiary.equipmentStatus; - if (equipmentStatus && ['ordered', 'shipped', 'delivered'].includes(equipmentStatus)) { - return 'awaiting_equipment'; - } - - // Check if has devices - required first step + // Check if has devices - used in multiple places const hasDevices = beneficiary.hasDevices || (beneficiary.devices && beneficiary.devices.length > 0) || 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'; // Check subscription - required after devices connected @@ -339,6 +472,23 @@ export default function BeneficiaryDetailScreen() { 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) useEffect(() => { if (isLocal && id && beneficiary) { @@ -361,12 +511,26 @@ export default function BeneficiaryDetailScreen() { }); }; - const handleGetSensors = () => { - // Navigate to purchase screen with beneficiary info - router.push({ - pathname: '/(auth)/purchase', - params: { beneficiaryId: id!, lovedOneName: beneficiary?.name }, - }); + const handlePurchaseSuccess = async () => { + // Update beneficiary with ordered status and subscription (kit includes 1 year) + if (id && isLocal) { + // Calculate subscription end date (1 year from now) + 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 () => { @@ -454,6 +618,26 @@ export default function BeneficiaryDetailScreen() { 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 = () => { if (!isLocal || !id) return; @@ -509,8 +693,11 @@ export default function BeneficiaryDetailScreen() { return ( ); @@ -524,6 +711,51 @@ export default function BeneficiaryDetailScreen() { case 'ready': default: + // WebView mode - uses test NDK deployment for demo data + if (showWebView) { + const webViewUrl = `${DASHBOARD_URL}?deployment_id=${TEST_NDK_DEPLOYMENT_ID}`; + return ( + + ( + + + Loading dashboard... + + )} + /> + {/* Developer Toggle - always visible to switch back */} + + + + + + Developer Mode + Show WebView dashboard + + + + + + + ); + } + + // Native mode return ( } > - {/* Profile Card */} - - - {beneficiary.avatar ? ( - - ) : ( - - - {beneficiary.name.charAt(0).toUpperCase()} - - - )} - - - - {beneficiary.name} - - {beneficiary.address && ( - - - {beneficiary.address} - - )} - - - - - {beneficiary.status === 'online' ? 'Online now' : 'Offline'} - - - - {beneficiary.last_activity && ( - - Last activity: {beneficiary.last_activity} - - )} - - - {/* Quick Actions */} - Quick Actions - - { - setCurrentBeneficiary(beneficiary); - router.push('/(tabs)/chat'); - }} - > - - - - Chat with Julia - - - router.push(`/(tabs)/beneficiaries/${id}/equipment`)} - > - - - - Equipment - - - - {/* Subscription Section */} - Subscription - - - - - - - - WellNuo Pro - Active - - - - $49 - /mo + {/* Developer Toggle for WebView */} + + + + + Developer Mode + Show WebView dashboard - - - - - - - 24/7 AI monitoring - - - - Unlimited chat with Julia - - - - Activity reports - - - - showComingSoon('Subscription Management')} - > - Manage Subscription - - + - {/* Settings */} - Settings - - showComingSoon('Notification Settings')}> - - - - - Notifications - - - - - - - showComingSoon('Alert Rules')}> - - - - - Alert Rules - - - - - - - showComingSoon('Connected Sensors')}> - - - - - Connected Sensors - - - - - - {/* Danger Zone - only for local beneficiaries */} - {isLocal && ( - <> - Danger Zone - - - Remove Beneficiary - - - )} - {/* Activity Dashboard */} @@ -710,7 +800,24 @@ export default function BeneficiaryDetailScreen() { router.back()}> - {beneficiary.name} + + {/* Avatar + Name */} + + + {beneficiary.avatar ? ( + + ) : ( + + + {beneficiary.name.charAt(0).toUpperCase()} + + + )} + + + {beneficiary.name} + + setIsMenuVisible(!isMenuVisible)}> @@ -926,6 +1033,98 @@ const styles = StyleSheet.create({ fontWeight: FontWeights.semibold, 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: { flex: 1, }, @@ -934,11 +1133,13 @@ const styles = StyleSheet.create({ paddingBottom: Spacing.xxl, }, // Setup Screens + setupScrollContent: { + flexGrow: 1, + justifyContent: 'center', + }, setupContainer: { - flex: 1, padding: Spacing.xl, alignItems: 'center', - justifyContent: 'center', }, setupIconContainer: { width: 96, @@ -1057,6 +1258,20 @@ const styles = StyleSheet.create({ fontWeight: FontWeights.semibold, 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: { flexDirection: 'row', alignItems: 'center', diff --git a/app/(tabs)/beneficiaries/_layout.tsx b/app/(tabs)/beneficiaries/_layout.tsx index b323b95..2202c19 100644 --- a/app/(tabs)/beneficiaries/_layout.tsx +++ b/app/(tabs)/beneficiaries/_layout.tsx @@ -10,7 +10,6 @@ export default function BeneficiariesLayout() { }} > - ); }