Compare commits
No commits in common. "2e723988185b76ca52c292d59025ed8dbe8393f9" and "79baf86faffa71953071dab790060cd38e9850a5" have entirely different histories.
2e72398818
...
79baf86faf
@ -1,3 +0,0 @@
|
||||
appId: host.exp.Exponent
|
||||
---
|
||||
- tapOn: "Mama3030"
|
||||
@ -93,10 +93,8 @@ export default function AddLovedOneScreen() {
|
||||
|
||||
try {
|
||||
// Create beneficiary on server IMMEDIATELY
|
||||
const trimmedAddress = address.trim();
|
||||
const result = await api.createBeneficiary({
|
||||
name: trimmedName,
|
||||
address: trimmedAddress || undefined,
|
||||
});
|
||||
|
||||
if (!result.ok || !result.data) {
|
||||
@ -106,15 +104,6 @@ export default function AddLovedOneScreen() {
|
||||
|
||||
const beneficiaryId = result.data.id;
|
||||
|
||||
// Upload avatar if selected
|
||||
if (avatarUri) {
|
||||
const avatarResult = await api.updateBeneficiaryAvatar(beneficiaryId, avatarUri);
|
||||
if (!avatarResult.ok) {
|
||||
console.warn('[AddLovedOne] Failed to upload avatar:', avatarResult.error?.message);
|
||||
// Continue anyway - avatar is not critical
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to the purchase/subscription screen with beneficiary ID
|
||||
router.replace({
|
||||
pathname: '/(auth)/purchase',
|
||||
@ -122,6 +111,7 @@ export default function AddLovedOneScreen() {
|
||||
beneficiaryId: String(beneficiaryId),
|
||||
lovedOneName: trimmedName,
|
||||
lovedOneAddress: address.trim(),
|
||||
lovedOneAvatar: avatarUri || '',
|
||||
inviteCode,
|
||||
},
|
||||
});
|
||||
|
||||
@ -164,9 +164,7 @@ export default function PurchaseScreen() {
|
||||
}
|
||||
|
||||
await api.setOnboardingCompleted(true);
|
||||
|
||||
// Redirect directly to equipment-status page (skip order_placed screen)
|
||||
router.replace(`/(tabs)/beneficiaries/${beneficiaryId}/equipment-status`);
|
||||
setStep('order_placed');
|
||||
} catch (error) {
|
||||
console.error('Payment error:', error);
|
||||
Alert.alert(
|
||||
|
||||
@ -326,15 +326,6 @@ export default function EquipmentStatusScreen() {
|
||||
<Text style={styles.supportText}>Need help? Contact support</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={AppColors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Back to Loved Ones Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.backToLovedOnesButton}
|
||||
onPress={() => router.replace('/(tabs)/beneficiaries')}
|
||||
>
|
||||
<Ionicons name="arrow-back" size={20} color={AppColors.primary} />
|
||||
<Text style={styles.backToLovedOnesText}>Back to My Loved Ones</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@ -548,22 +539,4 @@ const styles = StyleSheet.create({
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textSecondary,
|
||||
},
|
||||
// Back to Loved Ones Button
|
||||
backToLovedOnesButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: AppColors.surface,
|
||||
paddingVertical: Spacing.md,
|
||||
borderRadius: BorderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: AppColors.border,
|
||||
gap: Spacing.sm,
|
||||
marginTop: Spacing.lg,
|
||||
},
|
||||
backToLovedOnesText: {
|
||||
fontSize: FontSizes.base,
|
||||
fontWeight: FontWeights.medium,
|
||||
color: AppColors.primary,
|
||||
},
|
||||
});
|
||||
|
||||
@ -41,7 +41,6 @@ import {
|
||||
hasActiveSubscription,
|
||||
shouldShowSubscriptionWarning,
|
||||
} from '@/services/BeneficiaryDetailController';
|
||||
import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu';
|
||||
|
||||
// WebView Dashboard URL - opens specific deployment directly
|
||||
const getDashboardUrl = (deploymentId?: number) => {
|
||||
@ -61,14 +60,10 @@ export default function BeneficiaryDetailScreen() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isMenuVisible, setIsMenuVisible] = useState(false);
|
||||
const [showWebView, setShowWebView] = useState(false);
|
||||
const [isWebViewReady, setIsWebViewReady] = useState(false);
|
||||
const [legacyCredentials, setLegacyCredentials] = useState<{
|
||||
token: string;
|
||||
userName: string;
|
||||
userId: string;
|
||||
} | null>(null);
|
||||
const [isRefreshingToken, setIsRefreshingToken] = useState(false);
|
||||
const [authToken, setAuthToken] = useState<string | null>(null);
|
||||
|
||||
// Edit modal state
|
||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
||||
@ -76,66 +71,20 @@ export default function BeneficiaryDetailScreen() {
|
||||
|
||||
const webViewRef = useRef<WebView>(null);
|
||||
|
||||
// Load legacy credentials for WebView dashboard
|
||||
const loadLegacyCredentials = useCallback(async () => {
|
||||
try {
|
||||
// Check if token is expiring soon
|
||||
const isExpiring = await api.isLegacyTokenExpiringSoon();
|
||||
if (isExpiring) {
|
||||
console.log('[DevMode] Legacy token expiring, refreshing...');
|
||||
await api.refreshLegacyToken();
|
||||
}
|
||||
|
||||
const credentials = await api.getLegacyWebViewCredentials();
|
||||
if (credentials) {
|
||||
setLegacyCredentials(credentials);
|
||||
console.log('[DevMode] Legacy credentials loaded:', credentials.userName);
|
||||
}
|
||||
setIsWebViewReady(true);
|
||||
} catch (err) {
|
||||
console.log('[DevMode] Failed to load legacy credentials:', err);
|
||||
setIsWebViewReady(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load legacy token for WebView dashboard
|
||||
useEffect(() => {
|
||||
loadLegacyCredentials();
|
||||
|
||||
// Periodically refresh token (every 30 minutes)
|
||||
const tokenCheckInterval = setInterval(async () => {
|
||||
if (!showWebView) return; // Only refresh if WebView is active
|
||||
|
||||
const isExpiring = await api.isLegacyTokenExpiringSoon();
|
||||
if (isExpiring && !isRefreshingToken) {
|
||||
console.log('[DevMode] Periodic check: refreshing legacy token...');
|
||||
setIsRefreshingToken(true);
|
||||
const result = await api.refreshLegacyToken();
|
||||
if (result.ok) {
|
||||
const credentials = await api.getLegacyWebViewCredentials();
|
||||
if (credentials) {
|
||||
setLegacyCredentials(credentials);
|
||||
// Re-inject token into WebView
|
||||
const injectScript = `
|
||||
(function() {
|
||||
var authData = {
|
||||
username: '${credentials.userName}',
|
||||
token: '${credentials.token}',
|
||||
user_id: ${credentials.userId}
|
||||
};
|
||||
localStorage.setItem('auth2', JSON.stringify(authData));
|
||||
console.log('Token auto-refreshed');
|
||||
})();
|
||||
true;
|
||||
`;
|
||||
webViewRef.current?.injectJavaScript(injectScript);
|
||||
}
|
||||
}
|
||||
setIsRefreshingToken(false);
|
||||
const loadLegacyToken = async () => {
|
||||
try {
|
||||
const token = await api.getLegacyToken();
|
||||
setAuthToken(token);
|
||||
setIsWebViewReady(true);
|
||||
} catch (err) {
|
||||
console.log('[BeneficiaryDetail] Legacy token not available');
|
||||
setIsWebViewReady(true);
|
||||
}
|
||||
}, 30 * 60 * 1000); // 30 minutes
|
||||
|
||||
return () => clearInterval(tokenCheckInterval);
|
||||
}, [loadLegacyCredentials, showWebView, isRefreshingToken]);
|
||||
};
|
||||
loadLegacyToken();
|
||||
}, []);
|
||||
|
||||
const loadBeneficiary = useCallback(async (showLoadingIndicator = true) => {
|
||||
if (!id) return;
|
||||
@ -230,33 +179,19 @@ export default function BeneficiaryDetailScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
const beneficiaryId = parseInt(id, 10);
|
||||
|
||||
try {
|
||||
// Update basic info
|
||||
const response = await api.updateWellNuoBeneficiary(beneficiaryId, {
|
||||
const response = await api.updateWellNuoBeneficiary(parseInt(id, 10), {
|
||||
name: editForm.name.trim(),
|
||||
address: editForm.address.trim() || undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.ok) {
|
||||
setIsEditModalVisible(false);
|
||||
toast.success('Saved', 'Profile updated successfully');
|
||||
loadBeneficiary(false);
|
||||
} else {
|
||||
toast.error('Error', response.error?.message || 'Failed to save changes.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload avatar if changed (new local file URI)
|
||||
if (editForm.avatar && editForm.avatar.startsWith('file://')) {
|
||||
const avatarResult = await api.updateBeneficiaryAvatar(beneficiaryId, editForm.avatar);
|
||||
if (!avatarResult.ok) {
|
||||
console.warn('[BeneficiaryDetail] Failed to upload avatar:', avatarResult.error?.message);
|
||||
// Show info but don't fail the whole operation
|
||||
toast.info('Note', 'Profile saved but avatar upload failed');
|
||||
}
|
||||
}
|
||||
|
||||
setIsEditModalVisible(false);
|
||||
toast.success('Saved', 'Profile updated successfully');
|
||||
loadBeneficiary(false);
|
||||
} catch (err) {
|
||||
toast.error('Error', 'Failed to save changes.');
|
||||
}
|
||||
@ -292,18 +227,15 @@ export default function BeneficiaryDetailScreen() {
|
||||
};
|
||||
|
||||
// JavaScript to inject token into localStorage for WebView
|
||||
// Web app expects auth2 as JSON: {username, token, user_id}
|
||||
const injectedJavaScript = legacyCredentials
|
||||
const injectedJavaScript = authToken
|
||||
? `
|
||||
(function() {
|
||||
try {
|
||||
var authData = {
|
||||
username: '${legacyCredentials.userName}',
|
||||
token: '${legacyCredentials.token}',
|
||||
user_id: ${legacyCredentials.userId}
|
||||
token: '${authToken}'
|
||||
};
|
||||
localStorage.setItem('auth2', JSON.stringify(authData));
|
||||
console.log('Auth data injected:', authData.username);
|
||||
console.log('Auth data injected');
|
||||
} catch(e) {
|
||||
console.error('Failed to inject token:', e);
|
||||
}
|
||||
@ -347,14 +279,83 @@ export default function BeneficiaryDetailScreen() {
|
||||
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
|
||||
</View>
|
||||
|
||||
<BeneficiaryMenu
|
||||
beneficiaryId={id || ''}
|
||||
onEdit={handleEditPress}
|
||||
onRemove={handleDeleteBeneficiary}
|
||||
/>
|
||||
<View>
|
||||
<TouchableOpacity style={styles.headerButton} 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="share-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}/subscription`);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="diamond-outline" size={20} color={AppColors.textPrimary} />
|
||||
<Text style={styles.dropdownItemText}>Subscription</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>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.dropdownItem, styles.dropdownItemDanger]}
|
||||
onPress={() => {
|
||||
setIsMenuVisible(false);
|
||||
handleDeleteBeneficiary();
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={20} color={AppColors.error} />
|
||||
<Text style={[styles.dropdownItemText, styles.dropdownItemTextDanger]}>Remove</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* DEBUG PANEL - commented out
|
||||
{/* Backdrop to close menu */}
|
||||
{isMenuVisible && (
|
||||
<TouchableOpacity
|
||||
style={styles.menuBackdrop}
|
||||
activeOpacity={1}
|
||||
onPress={() => setIsMenuVisible(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* DEBUG PANEL */}
|
||||
{__DEV__ && (
|
||||
<View style={styles.debugPanel}>
|
||||
<Text style={styles.debugTitle}>DEBUG INFO (tap to copy)</Text>
|
||||
@ -376,7 +377,6 @@ export default function BeneficiaryDetailScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
*/}
|
||||
|
||||
{/* Dashboard Content */}
|
||||
<View style={styles.dashboardContainer}>
|
||||
@ -397,10 +397,10 @@ export default function BeneficiaryDetailScreen() {
|
||||
{/* Content area - WebView or MockDashboard */}
|
||||
<View style={styles.dashboardContent}>
|
||||
{showWebView ? (
|
||||
isWebViewReady && legacyCredentials ? (
|
||||
isWebViewReady && authToken ? (
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ uri: getDashboardUrl(api.getDemoDeploymentId()) }}
|
||||
source={{ uri: getDashboardUrl(FERDINAND_DEPLOYMENT_ID) }}
|
||||
style={styles.webView}
|
||||
javaScriptEnabled={true}
|
||||
domStorageEnabled={true}
|
||||
@ -422,7 +422,7 @@ export default function BeneficiaryDetailScreen() {
|
||||
<View style={styles.webViewLoading}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
<Text style={styles.webViewLoadingText}>
|
||||
{isWebViewReady ? 'Authenticating...' : 'Connecting to sensors...'}
|
||||
{isWebViewReady ? 'Loading dashboard...' : 'Connecting to sensors...'}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
@ -571,6 +571,42 @@ const styles = StyleSheet.create({
|
||||
fontWeight: FontWeights.semibold,
|
||||
color: AppColors.textPrimary,
|
||||
},
|
||||
// Dropdown Menu
|
||||
dropdownMenu: {
|
||||
position: 'absolute',
|
||||
top: 44,
|
||||
right: 0,
|
||||
backgroundColor: AppColors.surface,
|
||||
borderRadius: BorderRadius.lg,
|
||||
minWidth: 160,
|
||||
...Shadows.lg,
|
||||
zIndex: 100,
|
||||
},
|
||||
dropdownItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: Spacing.md,
|
||||
gap: Spacing.sm,
|
||||
},
|
||||
dropdownItemText: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textPrimary,
|
||||
},
|
||||
dropdownItemDanger: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: AppColors.border,
|
||||
},
|
||||
dropdownItemTextDanger: {
|
||||
color: AppColors.error,
|
||||
},
|
||||
menuBackdrop: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 5,
|
||||
},
|
||||
// Debug Panel
|
||||
debugPanel: {
|
||||
backgroundColor: '#FFF9C4',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,3 @@
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
|
||||
const { Pool } = require('pg');
|
||||
|
||||
// PostgreSQL connection to eluxnetworks.net
|
||||
|
||||
@ -46,16 +46,12 @@ async function getStripeSubscriptionStatus(stripeCustomerId) {
|
||||
|
||||
if (subscriptions.data.length > 0) {
|
||||
const sub = subscriptions.data[0];
|
||||
// Use cancel_at if subscription is set to cancel, otherwise use current_period_end
|
||||
const periodEndTimestamp = sub.cancel_at || sub.current_period_end;
|
||||
const endDate = periodEndTimestamp ? new Date(periodEndTimestamp * 1000).toISOString() : null;
|
||||
|
||||
return {
|
||||
plan: 'premium',
|
||||
status: 'active',
|
||||
hasSubscription: true,
|
||||
endDate: endDate,
|
||||
cancelAtPeriodEnd: sub.cancel_at_period_end || false
|
||||
currentPeriodEnd: new Date(sub.current_period_end * 1000).toISOString(),
|
||||
cancelAtPeriodEnd: sub.cancel_at_period_end
|
||||
};
|
||||
}
|
||||
|
||||
@ -68,15 +64,11 @@ async function getStripeSubscriptionStatus(stripeCustomerId) {
|
||||
if (allSubs.data.length > 0) {
|
||||
const sub = allSubs.data[0];
|
||||
const normalizedStatus = normalizeStripeStatus(sub.status);
|
||||
const periodEndTimestamp = sub.cancel_at || sub.current_period_end;
|
||||
const endDate = periodEndTimestamp ? new Date(periodEndTimestamp * 1000).toISOString() : null;
|
||||
|
||||
return {
|
||||
plan: normalizedStatus === 'canceled' || normalizedStatus === 'none' || normalizedStatus === 'expired' ? 'free' : 'premium',
|
||||
status: normalizedStatus,
|
||||
hasSubscription: normalizedStatus === 'active' || normalizedStatus === 'trialing',
|
||||
endDate: endDate,
|
||||
cancelAtPeriodEnd: sub.cancel_at_period_end || false
|
||||
currentPeriodEnd: new Date(sub.current_period_end * 1000).toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
@ -144,15 +136,12 @@ router.get('/', async (req, res) => {
|
||||
}
|
||||
|
||||
// Query from beneficiaries table (new architecture)
|
||||
console.log('[GET BENEFICIARIES] querying beneficiaries table for id:', beneficiaryTableId);
|
||||
const { data: beneficiary, error: beneficiaryError } = await supabase
|
||||
const { data: beneficiary } = await supabase
|
||||
.from('beneficiaries')
|
||||
.select('id, name, phone, address, avatar_url, created_at, equipment_status, stripe_customer_id')
|
||||
.select('id, name, phone, address_street, address_city, address_zip, address_state, address_country, created_at, equipment_status, stripe_customer_id')
|
||||
.eq('id', beneficiaryTableId)
|
||||
.single();
|
||||
|
||||
console.log('[GET BENEFICIARIES] got beneficiary:', beneficiary ? beneficiary.name : null, 'error:', beneficiaryError);
|
||||
|
||||
if (beneficiary) {
|
||||
const subscription = await getStripeSubscriptionStatus(beneficiary.stripe_customer_id);
|
||||
beneficiaries.push({
|
||||
@ -162,8 +151,13 @@ router.get('/', async (req, res) => {
|
||||
grantedAt: record.granted_at,
|
||||
name: beneficiary.name,
|
||||
phone: beneficiary.phone,
|
||||
address: beneficiary.address || null,
|
||||
avatarUrl: beneficiary.avatar_url,
|
||||
address: {
|
||||
street: beneficiary.address_street,
|
||||
city: beneficiary.address_city,
|
||||
zip: beneficiary.address_zip,
|
||||
state: beneficiary.address_state,
|
||||
country: beneficiary.address_country
|
||||
},
|
||||
createdAt: beneficiary.created_at,
|
||||
subscription: subscription,
|
||||
// Equipment status from beneficiaries table - CRITICAL for navigation!
|
||||
@ -229,8 +223,13 @@ router.get('/:id', async (req, res) => {
|
||||
id: beneficiary.id,
|
||||
name: beneficiary.name,
|
||||
phone: beneficiary.phone,
|
||||
address: beneficiary.address || null,
|
||||
avatarUrl: beneficiary.avatar_url,
|
||||
address: {
|
||||
street: beneficiary.address_street,
|
||||
city: beneficiary.address_city,
|
||||
zip: beneficiary.address_zip,
|
||||
state: beneficiary.address_state,
|
||||
country: beneficiary.address_country
|
||||
},
|
||||
role: access.role,
|
||||
subscription: subscription,
|
||||
orders: orders || [],
|
||||
@ -267,7 +266,11 @@ router.post('/', async (req, res) => {
|
||||
.insert({
|
||||
name: name,
|
||||
phone: phone || null,
|
||||
address: address || null,
|
||||
address_street: address?.street || null,
|
||||
address_city: address?.city || null,
|
||||
address_zip: address?.zip || null,
|
||||
address_state: address?.state || null,
|
||||
address_country: address?.country || null,
|
||||
equipment_status: 'none',
|
||||
created_by: userId,
|
||||
created_at: new Date().toISOString(),
|
||||
@ -310,8 +313,13 @@ router.post('/', async (req, res) => {
|
||||
id: beneficiary.id,
|
||||
name: beneficiary.name,
|
||||
phone: beneficiary.phone,
|
||||
address: beneficiary.address || null,
|
||||
avatarUrl: beneficiary.avatar_url,
|
||||
address: {
|
||||
street: beneficiary.address_street,
|
||||
city: beneficiary.address_city,
|
||||
zip: beneficiary.address_zip,
|
||||
state: beneficiary.address_state,
|
||||
country: beneficiary.address_country
|
||||
},
|
||||
role: 'custodian',
|
||||
equipmentStatus: 'none'
|
||||
}
|
||||
@ -333,8 +341,6 @@ router.patch('/:id', async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const beneficiaryId = parseInt(req.params.id, 10);
|
||||
|
||||
console.log('[BENEFICIARY PATCH] Request:', { userId, beneficiaryId, body: req.body });
|
||||
|
||||
// Check user has custodian or guardian access - using beneficiary_id
|
||||
const { data: access, error: accessError } = await supabase
|
||||
.from('user_access')
|
||||
@ -347,7 +353,7 @@ router.patch('/:id', async (req, res) => {
|
||||
return res.status(403).json({ error: 'Only custodian or guardian can update beneficiary info' });
|
||||
}
|
||||
|
||||
const { name, phone, address } = req.body;
|
||||
const { name, phone, addressStreet, addressCity, addressZip, addressState, addressCountry } = req.body;
|
||||
|
||||
const updateData = {
|
||||
updated_at: new Date().toISOString()
|
||||
@ -355,7 +361,11 @@ router.patch('/:id', async (req, res) => {
|
||||
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (phone !== undefined) updateData.phone = phone;
|
||||
if (address !== undefined) updateData.address = address;
|
||||
if (addressStreet !== undefined) updateData.address_street = addressStreet;
|
||||
if (addressCity !== undefined) updateData.address_city = addressCity;
|
||||
if (addressZip !== undefined) updateData.address_zip = addressZip;
|
||||
if (addressState !== undefined) updateData.address_state = addressState;
|
||||
if (addressCountry !== undefined) updateData.address_country = addressCountry;
|
||||
|
||||
// Update in beneficiaries table
|
||||
const { data: beneficiary, error } = await supabase
|
||||
@ -366,25 +376,27 @@ router.patch('/:id', async (req, res) => {
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('[BENEFICIARY PATCH] Supabase error:', error);
|
||||
return res.status(500).json({ error: 'Failed to update beneficiary' });
|
||||
}
|
||||
|
||||
console.log('[BENEFICIARY PATCH] Success:', { id: beneficiary.id, name: beneficiary.name, address: beneficiary.address });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
beneficiary: {
|
||||
id: beneficiary.id,
|
||||
name: beneficiary.name,
|
||||
phone: beneficiary.phone,
|
||||
address: beneficiary.address || null,
|
||||
avatarUrl: beneficiary.avatar_url
|
||||
address: {
|
||||
street: beneficiary.address_street,
|
||||
city: beneficiary.address_city,
|
||||
zip: beneficiary.address_zip,
|
||||
state: beneficiary.address_state,
|
||||
country: beneficiary.address_country
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[BENEFICIARY PATCH] Error:', error);
|
||||
console.error('Update beneficiary error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
@ -701,7 +713,7 @@ router.post('/:id/activate', async (req, res) => {
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', beneficiaryId)
|
||||
.select('id, name, equipment_status')
|
||||
.select('id, first_name, last_name, equipment_status')
|
||||
.single();
|
||||
|
||||
if (updateError) {
|
||||
@ -715,7 +727,8 @@ router.post('/:id/activate', async (req, res) => {
|
||||
success: true,
|
||||
beneficiary: {
|
||||
id: beneficiary?.id || beneficiaryId,
|
||||
name: beneficiary?.name || null,
|
||||
firstName: beneficiary?.first_name || null,
|
||||
lastName: beneficiary?.last_name || null,
|
||||
hasDevices: true,
|
||||
equipmentStatus: equipmentStatus
|
||||
}
|
||||
@ -815,68 +828,6 @@ router.post('/:id/transfer', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /api/me/beneficiaries/:id/avatar
|
||||
* Upload/update beneficiary avatar (base64 image)
|
||||
*/
|
||||
router.patch('/:id/avatar', async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const beneficiaryId = parseInt(req.params.id, 10);
|
||||
const { avatar } = req.body; // base64 string or null to remove
|
||||
|
||||
console.log('[BENEFICIARY] Avatar update:', { userId, beneficiaryId, hasAvatar: !!avatar });
|
||||
|
||||
// Check user has custodian or guardian access
|
||||
const { data: access, error: accessError } = await supabase
|
||||
.from('user_access')
|
||||
.select('role')
|
||||
.eq('accessor_id', userId)
|
||||
.eq('beneficiary_id', beneficiaryId)
|
||||
.single();
|
||||
|
||||
if (accessError || !access || !['custodian', 'guardian', 'caretaker'].includes(access.role)) {
|
||||
return res.status(403).json({ error: 'Only custodian, guardian or caretaker can update avatar' });
|
||||
}
|
||||
|
||||
// Validate base64 if provided
|
||||
if (avatar && !avatar.startsWith('data:image/')) {
|
||||
return res.status(400).json({ error: 'Invalid image format. Must be base64 data URI' });
|
||||
}
|
||||
|
||||
// Update avatar_url in beneficiaries table
|
||||
const { data: beneficiary, error } = await supabase
|
||||
.from('beneficiaries')
|
||||
.update({
|
||||
avatar_url: avatar || null,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', beneficiaryId)
|
||||
.select('id, name, avatar_url')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('[BENEFICIARY] Avatar update error:', error);
|
||||
return res.status(500).json({ error: 'Failed to update avatar' });
|
||||
}
|
||||
|
||||
console.log('[BENEFICIARY] Avatar updated:', { beneficiaryId, hasAvatar: !!beneficiary.avatar_url });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
beneficiary: {
|
||||
id: beneficiary.id,
|
||||
name: beneficiary.name,
|
||||
avatarUrl: beneficiary.avatar_url
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[BENEFICIARY] Avatar error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update equipment status for a beneficiary
|
||||
* PATCH /me/beneficiaries/:id/equipment-status
|
||||
@ -919,7 +870,7 @@ router.patch('/:id/equipment-status', authMiddleware, async (req, res) => {
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', beneficiaryId)
|
||||
.select('id, name, equipment_status')
|
||||
.select('id, first_name, equipment_status')
|
||||
.single();
|
||||
|
||||
if (updateError) {
|
||||
@ -937,7 +888,7 @@ router.patch('/:id/equipment-status', authMiddleware, async (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
id: updated.id,
|
||||
name: updated.name,
|
||||
firstName: updated.first_name,
|
||||
equipmentStatus: updated.equipment_status
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@ -424,23 +424,19 @@ router.get('/subscription-status/:beneficiaryId', async (req, res) => {
|
||||
router.post('/cancel-subscription', async (req, res) => {
|
||||
try {
|
||||
const { beneficiaryId } = req.body;
|
||||
console.log('[CANCEL] Request received for beneficiaryId:', beneficiaryId);
|
||||
|
||||
if (!beneficiaryId) {
|
||||
return res.status(400).json({ error: 'beneficiaryId is required' });
|
||||
}
|
||||
|
||||
// Get beneficiary's stripe_customer_id
|
||||
const { data: beneficiary, error: dbError } = await supabase
|
||||
const { data: beneficiary } = await supabase
|
||||
.from('beneficiaries')
|
||||
.select('stripe_customer_id')
|
||||
.eq('id', beneficiaryId)
|
||||
.single();
|
||||
|
||||
console.log('[CANCEL] DB result:', { beneficiary, dbError });
|
||||
|
||||
if (!beneficiary?.stripe_customer_id) {
|
||||
console.log('[CANCEL] No stripe_customer_id found');
|
||||
return res.status(404).json({ error: 'No subscription found' });
|
||||
}
|
||||
|
||||
@ -460,16 +456,12 @@ router.post('/cancel-subscription', async (req, res) => {
|
||||
cancel_at_period_end: true
|
||||
});
|
||||
|
||||
console.log(`✓ Subscription ${subscription.id} will cancel at period end:`, subscription.current_period_end);
|
||||
|
||||
const cancelAt = subscription.current_period_end
|
||||
? new Date(subscription.current_period_end * 1000).toISOString()
|
||||
: null;
|
||||
console.log(`✓ Subscription ${subscription.id} will cancel at period end`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Subscription will cancel at the end of the billing period',
|
||||
cancelAt
|
||||
cancelAt: new Date(subscription.current_period_end * 1000).toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@ -561,22 +553,6 @@ router.post('/create-subscription-payment-sheet', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel any incomplete subscriptions to avoid duplicates
|
||||
const incompleteSubs = await stripe.subscriptions.list({
|
||||
customer: customerId,
|
||||
status: 'incomplete',
|
||||
limit: 10
|
||||
});
|
||||
|
||||
for (const sub of incompleteSubs.data) {
|
||||
try {
|
||||
await stripe.subscriptions.cancel(sub.id);
|
||||
console.log(`Canceled incomplete subscription ${sub.id} for customer ${customerId}`);
|
||||
} catch (cancelError) {
|
||||
console.warn(`Failed to cancel incomplete subscription ${sub.id}:`, cancelError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Create ephemeral key
|
||||
const ephemeralKey = await stripe.ephemeralKeys.create(
|
||||
{ customer: customerId },
|
||||
@ -598,37 +574,11 @@ router.post('/create-subscription-payment-sheet', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Try to get payment_intent from expanded invoice
|
||||
let clientSecret = subscription.latest_invoice?.payment_intent?.client_secret;
|
||||
|
||||
// Stripe SDK v20+ doesn't expose payment_intent field in Invoice object
|
||||
// Need to fetch PaymentIntent via list API as a workaround
|
||||
if (!clientSecret && subscription.latest_invoice) {
|
||||
const invoiceId = typeof subscription.latest_invoice === 'string'
|
||||
? subscription.latest_invoice
|
||||
: subscription.latest_invoice.id;
|
||||
|
||||
if (invoiceId) {
|
||||
// List recent PaymentIntents for this customer and find the one for this invoice
|
||||
const paymentIntents = await stripe.paymentIntents.list({
|
||||
customer: customerId,
|
||||
limit: 5
|
||||
});
|
||||
|
||||
for (const pi of paymentIntents.data) {
|
||||
if (pi.invoice === invoiceId || pi.description?.includes('Subscription')) {
|
||||
clientSecret = pi.client_secret;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[SUBSCRIPTION] Created subscription ${subscription.id}, clientSecret: ${!!clientSecret}`);
|
||||
const paymentIntent = subscription.latest_invoice?.payment_intent;
|
||||
|
||||
res.json({
|
||||
subscriptionId: subscription.id,
|
||||
clientSecret: clientSecret,
|
||||
clientSecret: paymentIntent?.client_secret,
|
||||
ephemeralKey: ephemeralKey.secret,
|
||||
customer: customerId,
|
||||
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY
|
||||
@ -676,95 +626,6 @@ router.post('/confirm-subscription-payment', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/stripe/transaction-history/:beneficiaryId
|
||||
* Gets transaction/invoice history directly from Stripe
|
||||
*/
|
||||
router.get('/transaction-history/:beneficiaryId', async (req, res) => {
|
||||
try {
|
||||
const { beneficiaryId } = req.params;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
|
||||
// Get beneficiary's stripe_customer_id
|
||||
const { data: beneficiary } = await supabase
|
||||
.from('beneficiaries')
|
||||
.select('stripe_customer_id')
|
||||
.eq('id', beneficiaryId)
|
||||
.single();
|
||||
|
||||
if (!beneficiary?.stripe_customer_id) {
|
||||
return res.json({
|
||||
transactions: [],
|
||||
hasMore: false
|
||||
});
|
||||
}
|
||||
|
||||
// Get invoices from Stripe
|
||||
const invoices = await stripe.invoices.list({
|
||||
customer: beneficiary.stripe_customer_id,
|
||||
limit: limit,
|
||||
expand: ['data.subscription']
|
||||
});
|
||||
|
||||
// Also get PaymentIntents for one-time purchases (equipment)
|
||||
const paymentIntents = await stripe.paymentIntents.list({
|
||||
customer: beneficiary.stripe_customer_id,
|
||||
limit: limit
|
||||
});
|
||||
|
||||
// Format invoices (subscription payments) - only show paid invoices
|
||||
const formattedInvoices = invoices.data
|
||||
.filter(invoice => invoice.status === 'paid' && invoice.amount_paid > 0)
|
||||
.map(invoice => ({
|
||||
id: invoice.id,
|
||||
type: 'subscription',
|
||||
amount: invoice.amount_paid / 100,
|
||||
currency: invoice.currency.toUpperCase(),
|
||||
status: invoice.status,
|
||||
date: new Date(invoice.created * 1000).toISOString(),
|
||||
description: invoice.lines.data[0]?.description || 'WellNuo Premium',
|
||||
invoicePdf: invoice.invoice_pdf,
|
||||
hostedUrl: invoice.hosted_invoice_url
|
||||
}));
|
||||
|
||||
// Format payment intents (one-time purchases like equipment)
|
||||
// Exclude subscription-related payments (they're already in invoices)
|
||||
const formattedPayments = paymentIntents.data
|
||||
.filter(pi => {
|
||||
if (pi.status !== 'succeeded') return false;
|
||||
if (pi.invoice) return false; // Has linked invoice
|
||||
// Exclude "Subscription creation" - it's duplicate of invoice
|
||||
if (pi.description === 'Subscription creation') return false;
|
||||
return true;
|
||||
})
|
||||
.map(pi => ({
|
||||
id: pi.id,
|
||||
type: 'one_time',
|
||||
amount: pi.amount / 100,
|
||||
currency: pi.currency.toUpperCase(),
|
||||
status: pi.status,
|
||||
date: new Date(pi.created * 1000).toISOString(),
|
||||
description: pi.metadata?.orderType === 'starter_kit'
|
||||
? 'WellNuo Starter Kit'
|
||||
: (pi.description || 'One-time payment'),
|
||||
receiptUrl: pi.charges?.data[0]?.receipt_url
|
||||
}));
|
||||
|
||||
// Combine and sort by date (newest first)
|
||||
const allTransactions = [...formattedInvoices, ...formattedPayments]
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
|
||||
res.json({
|
||||
transactions: allTransactions,
|
||||
hasMore: invoices.has_more || paymentIntents.has_more
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Get transaction history error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/stripe/session/:sessionId
|
||||
* Get checkout session details (for success page)
|
||||
|
||||
@ -1,186 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Modal, Pressable } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import { AppColors, BorderRadius, FontSizes, Spacing, Shadows } from '@/constants/theme';
|
||||
|
||||
export type MenuItemId = 'edit' | 'access' | 'subscription' | 'equipment' | 'remove';
|
||||
|
||||
interface MenuItem {
|
||||
id: MenuItemId;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
label: string;
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
const ALL_MENU_ITEMS: MenuItem[] = [
|
||||
{ id: 'edit', icon: 'create-outline', label: 'Edit' },
|
||||
{ id: 'access', icon: 'share-outline', label: 'Access' },
|
||||
{ id: 'subscription', icon: 'diamond-outline', label: 'Subscription' },
|
||||
{ id: 'equipment', icon: 'hardware-chip-outline', label: 'Equipment' },
|
||||
{ id: 'remove', icon: 'trash-outline', label: 'Remove', danger: true },
|
||||
];
|
||||
|
||||
interface BeneficiaryMenuProps {
|
||||
beneficiaryId: string | number;
|
||||
/** Which menu items to show. If not provided, shows all except current page */
|
||||
visibleItems?: MenuItemId[];
|
||||
/** Which menu item represents the current page (will be hidden) */
|
||||
currentPage?: MenuItemId;
|
||||
/** Custom handler for Edit action */
|
||||
onEdit?: () => void;
|
||||
/** Custom handler for Remove action */
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
export function BeneficiaryMenu({
|
||||
beneficiaryId,
|
||||
visibleItems,
|
||||
currentPage,
|
||||
onEdit,
|
||||
onRemove,
|
||||
}: BeneficiaryMenuProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const handleMenuAction = (itemId: MenuItemId) => {
|
||||
setIsVisible(false);
|
||||
|
||||
switch (itemId) {
|
||||
case 'edit':
|
||||
if (onEdit) {
|
||||
onEdit();
|
||||
} else {
|
||||
// Navigate to main page with edit intent
|
||||
router.push(`/(tabs)/beneficiaries/${beneficiaryId}`);
|
||||
}
|
||||
break;
|
||||
case 'access':
|
||||
router.push(`/(tabs)/beneficiaries/${beneficiaryId}/share`);
|
||||
break;
|
||||
case 'subscription':
|
||||
router.push(`/(tabs)/beneficiaries/${beneficiaryId}/subscription`);
|
||||
break;
|
||||
case 'equipment':
|
||||
router.push(`/(tabs)/beneficiaries/${beneficiaryId}/equipment`);
|
||||
break;
|
||||
case 'remove':
|
||||
if (onRemove) {
|
||||
onRemove();
|
||||
} else {
|
||||
// Navigate to main page
|
||||
router.push(`/(tabs)/beneficiaries/${beneficiaryId}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Filter menu items - only hide current page
|
||||
let menuItems = ALL_MENU_ITEMS;
|
||||
|
||||
if (visibleItems) {
|
||||
menuItems = ALL_MENU_ITEMS.filter(item => visibleItems.includes(item.id));
|
||||
} else if (currentPage) {
|
||||
menuItems = ALL_MENU_ITEMS.filter(item => item.id !== currentPage);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
style={styles.menuButton}
|
||||
onPress={() => setIsVisible(!isVisible)}
|
||||
>
|
||||
<Ionicons name="ellipsis-vertical" size={22} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Modal
|
||||
visible={isVisible}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
onRequestClose={() => setIsVisible(false)}
|
||||
>
|
||||
{/* Full screen backdrop */}
|
||||
<Pressable
|
||||
style={styles.modalBackdrop}
|
||||
onPress={() => setIsVisible(false)}
|
||||
>
|
||||
{/* Menu positioned at top right */}
|
||||
<Pressable
|
||||
style={styles.dropdownMenuContainer}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
>
|
||||
<View style={styles.dropdownMenu}>
|
||||
{menuItems.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={[
|
||||
styles.dropdownItem,
|
||||
item.danger && styles.dropdownItemDanger,
|
||||
]}
|
||||
onPress={() => handleMenuAction(item.id)}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={20}
|
||||
color={item.danger ? AppColors.error : AppColors.textPrimary}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.dropdownItemText,
|
||||
item.danger && styles.dropdownItemTextDanger,
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
menuButton: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
modalBackdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
dropdownMenuContainer: {
|
||||
position: 'absolute',
|
||||
top: 100, // Below status bar and header
|
||||
right: Spacing.md,
|
||||
},
|
||||
dropdownMenu: {
|
||||
backgroundColor: AppColors.surface,
|
||||
borderRadius: BorderRadius.lg,
|
||||
minWidth: 180,
|
||||
overflow: 'hidden',
|
||||
...Shadows.lg,
|
||||
},
|
||||
dropdownItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: Spacing.md,
|
||||
paddingHorizontal: Spacing.md,
|
||||
gap: Spacing.sm,
|
||||
width: '100%',
|
||||
},
|
||||
dropdownItemText: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textPrimary,
|
||||
},
|
||||
dropdownItemDanger: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: AppColors.border,
|
||||
},
|
||||
dropdownItemTextDanger: {
|
||||
color: AppColors.error,
|
||||
},
|
||||
});
|
||||
@ -1,19 +0,0 @@
|
||||
Starting project at /Users/sergei/Desktop/WellNuo
|
||||
/Users/sergei/Desktop/WellNuo/node_modules/expo/node_modules/@expo/cli/build/src/utils/errors.js:130
|
||||
throw error;
|
||||
^
|
||||
|
||||
RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type number (65536).
|
||||
at Server.listen (node:net:2059:5)
|
||||
at /Users/sergei/Desktop/WellNuo/node_modules/freeport-async/index.js:8:12
|
||||
at new Promise (<anonymous>)
|
||||
at testPortAsync (/Users/sergei/Desktop/WellNuo/node_modules/freeport-async/index.js:6:10)
|
||||
at availableAsync (/Users/sergei/Desktop/WellNuo/node_modules/freeport-async/index.js:24:17)
|
||||
at /Users/sergei/Desktop/WellNuo/node_modules/freeport-async/index.js:37:23
|
||||
at new Promise (<anonymous>)
|
||||
at freePortRangeAsync (/Users/sergei/Desktop/WellNuo/node_modules/freeport-async/index.js:33:10)
|
||||
at /Users/sergei/Desktop/WellNuo/node_modules/freeport-async/index.js:43:18 {
|
||||
code: 'ERR_SOCKET_BAD_PORT'
|
||||
}
|
||||
|
||||
Node.js v20.19.5
|
||||
226
expo-8081.log
226
expo-8081.log
@ -1,226 +0,0 @@
|
||||
Starting project at /Users/sergei/Desktop/WellNuo
|
||||
React Compiler enabled
|
||||
Starting Metro Bundler
|
||||
The following packages should be updated for best compatibility with the installed expo version:
|
||||
expo@54.0.30 - expected version: ~54.0.31
|
||||
expo-constants@18.0.12 - expected version: ~18.0.13
|
||||
Your project may not work correctly until you install the expected versions of the packages.
|
||||
› Opening exp://172.20.10.10:8081 on 8081
|
||||
Waiting on http://localhost:8081
|
||||
Logs for your project will appear below.
|
||||
Could not fetch new Expo development certificate, falling back to cached certificate
|
||||
iOS Bundled 531ms node_modules/expo-router/entry.js (1421 modules)
|
||||
LOG expo-speech-recognition not available
|
||||
LOG [Voice] expo-speech-recognition not available in Expo Go
|
||||
LOG [Layout] Still initializing auth state...
|
||||
LOG [AuthContext] checkAuth starting...
|
||||
LOG [AuthContext] checkAuth: Checking token...
|
||||
LOG [AuthContext] checkAuth: Token exists=true, length=187
|
||||
LOG [AuthContext] checkAuth: isAuth=true
|
||||
LOG [AuthContext] checkAuth: Getting stored user...
|
||||
LOG [API] getStoredUser: token exists = true , userId = 62
|
||||
LOG [API] getStoredUser: Fetching profile from server...
|
||||
LOG [API] getStoredUser: Profile response ok = true , error = undefined
|
||||
LOG [AuthContext] checkAuth: User found=true
|
||||
LOG [AuthContext] checkAuth: Finished
|
||||
LOG [Layout] Auth check: {"hasInitialRedirect": false, "inAuthGroup": false, "isAuthenticated": true}
|
||||
LOG [API] getAllBeneficiaries - token exists: true length: 187
|
||||
LOG [API] getAllBeneficiaries - Fetching from: https://wellnuo.smartlaunchhub.com/api/me/beneficiaries
|
||||
LOG [API] getAllBeneficiaries - Response status: 200
|
||||
LOG [API] getAllBeneficiaries - Data: {"beneficiaries":[{"accessId":41,"id":21,"role":"custodian","grantedAt":"2026-01-09T04:15:44.403Z","name":"Mama","phone":null,"address":null,"avatarUrl":null,"createdAt":"2026-01-09T04:15:43.079Z","su
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":21,"name":"Mama","hasDevices":false,"equipmentStatus":"none"}
|
||||
LOG [API] getAllBeneficiaries - token exists: true length: 187
|
||||
LOG [API] getAllBeneficiaries - Fetching from: https://wellnuo.smartlaunchhub.com/api/me/beneficiaries
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":21,"name":"Mama","hasDevices":false,"equipmentStatus":"none"}
|
||||
LOG [API] getAllBeneficiaries - Response status: 200
|
||||
LOG [API] getAllBeneficiaries - Data: {"beneficiaries":[{"accessId":41,"id":21,"role":"custodian","grantedAt":"2026-01-09T04:15:44.403Z","name":"Mama","phone":null,"address":null,"avatarUrl":null,"createdAt":"2026-01-09T04:15:43.079Z","su
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":22,"name":"Mama2","hasDevices":false,"equipmentStatus":"none"}
|
||||
LOG [Purchase] Creating payment sheet for userId: 62 beneficiaryId: 22
|
||||
ERROR Payment error: [Error: ephemeralKey is not defined]
|
||||
|
||||
Code: purchase.tsx
|
||||
[0m [90m 119 |[39m
|
||||
[90m 120 |[39m [36mif[39m ([33m![39mdata[33m.[39mpaymentIntent) {
|
||||
[31m[1m>[22m[39m[90m 121 |[39m [36mthrow[39m [36mnew[39m [33mError[39m(data[33m.[39merror [33m||[39m [32m'Failed to create payment sheet'[39m)[33m;[39m
|
||||
[90m |[39m [31m[1m^[22m[39m
|
||||
[90m 122 |[39m }
|
||||
[90m 123 |[39m
|
||||
[90m 124 |[39m [36mconst[39m { error[33m:[39m initError } [33m=[39m [36mawait[39m initPaymentSheet({[0m
|
||||
Call Stack
|
||||
handlePurchase (app/(auth)/purchase.tsx:121:24)
|
||||
LOG [Purchase] Creating payment sheet for userId: 62 beneficiaryId: 22
|
||||
ERROR Payment error: [Error: ephemeralKey is not defined]
|
||||
|
||||
Code: purchase.tsx
|
||||
[0m [90m 119 |[39m
|
||||
[90m 120 |[39m [36mif[39m ([33m![39mdata[33m.[39mpaymentIntent) {
|
||||
[31m[1m>[22m[39m[90m 121 |[39m [36mthrow[39m [36mnew[39m [33mError[39m(data[33m.[39merror [33m||[39m [32m'Failed to create payment sheet'[39m)[33m;[39m
|
||||
[90m |[39m [31m[1m^[22m[39m
|
||||
[90m 122 |[39m }
|
||||
[90m 123 |[39m
|
||||
[90m 124 |[39m [36mconst[39m { error[33m:[39m initError } [33m=[39m [36mawait[39m initPaymentSheet({[0m
|
||||
Call Stack
|
||||
handlePurchase (app/(auth)/purchase.tsx:121:24)
|
||||
Could not fetch new Expo development certificate, falling back to cached certificate
|
||||
iOS Bundled 83ms node_modules/expo-router/entry.js (1 module)
|
||||
LOG expo-speech-recognition not available
|
||||
LOG [Voice] expo-speech-recognition not available in Expo Go
|
||||
LOG [Layout] Still initializing auth state...
|
||||
LOG [AuthContext] checkAuth starting...
|
||||
LOG [AuthContext] checkAuth: Checking token...
|
||||
LOG [AuthContext] checkAuth: Token exists=true, length=187
|
||||
LOG [AuthContext] checkAuth: isAuth=true
|
||||
LOG [AuthContext] checkAuth: Getting stored user...
|
||||
LOG [API] getStoredUser: token exists = true , userId = 62
|
||||
LOG [API] getStoredUser: Fetching profile from server...
|
||||
LOG [API] getStoredUser: Profile response ok = true , error = undefined
|
||||
LOG [AuthContext] checkAuth: User found=true
|
||||
LOG [AuthContext] checkAuth: Finished
|
||||
LOG [Layout] Auth check: {"hasInitialRedirect": false, "inAuthGroup": false, "isAuthenticated": true}
|
||||
LOG [API] getAllBeneficiaries - token exists: true length: 187
|
||||
LOG [API] getAllBeneficiaries - Fetching from: https://wellnuo.smartlaunchhub.com/api/me/beneficiaries
|
||||
LOG [API] getAllBeneficiaries - Response status: 200
|
||||
LOG [API] getAllBeneficiaries - Data: {"beneficiaries":[{"accessId":41,"id":21,"role":"custodian","grantedAt":"2026-01-09T04:15:44.403Z","name":"Mama","phone":null,"address":null,"avatarUrl":null,"createdAt":"2026-01-09T04:15:43.079Z","su
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":22,"name":"Mama2","hasDevices":false,"equipmentStatus":"none"}
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":22,"name":"Mama2","hasDevices":false,"equipmentStatus":"none"}
|
||||
ERROR Payment error: [Error: ephemeralKey is not defined]
|
||||
|
||||
Code: purchase.tsx
|
||||
[0m [90m 113 |[39m
|
||||
[90m 114 |[39m [36mif[39m ([33m![39mdata[33m.[39mpaymentIntent) {
|
||||
[31m[1m>[22m[39m[90m 115 |[39m [36mthrow[39m [36mnew[39m [33mError[39m(data[33m.[39merror [33m||[39m [32m'Failed to create payment sheet'[39m)[33m;[39m
|
||||
[90m |[39m [31m[1m^[22m[39m
|
||||
[90m 116 |[39m }
|
||||
[90m 117 |[39m
|
||||
[90m 118 |[39m [36mconst[39m { error[33m:[39m initError } [33m=[39m [36mawait[39m initPaymentSheet({[0m
|
||||
Call Stack
|
||||
handlePurchase (app/(tabs)/beneficiaries/[id]/purchase.tsx:115:24)
|
||||
Could not fetch new Expo development certificate, falling back to cached certificate
|
||||
iOS node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
|
||||
iOS Bundled 68ms node_modules/expo-router/entry.js (1 module)
|
||||
LOG expo-speech-recognition not available
|
||||
LOG [Voice] expo-speech-recognition not available in Expo Go
|
||||
LOG [Layout] Still initializing auth state...
|
||||
LOG [AuthContext] checkAuth starting...
|
||||
LOG [AuthContext] checkAuth: Checking token...
|
||||
LOG [AuthContext] checkAuth: Token exists=true, length=187
|
||||
LOG [AuthContext] checkAuth: isAuth=true
|
||||
LOG [AuthContext] checkAuth: Getting stored user...
|
||||
LOG [API] getStoredUser: token exists = true , userId = 62
|
||||
LOG [API] getStoredUser: Fetching profile from server...
|
||||
LOG [API] getStoredUser: Profile response ok = true , error = undefined
|
||||
LOG [AuthContext] checkAuth: User found=true
|
||||
LOG [AuthContext] checkAuth: Finished
|
||||
LOG [Layout] Auth check: {"hasInitialRedirect": false, "inAuthGroup": false, "isAuthenticated": true}
|
||||
LOG [API] getAllBeneficiaries - token exists: true length: 187
|
||||
LOG [API] getAllBeneficiaries - Fetching from: https://wellnuo.smartlaunchhub.com/api/me/beneficiaries
|
||||
LOG [API] getAllBeneficiaries - Response status: 200
|
||||
LOG [API] getAllBeneficiaries - Data: {"beneficiaries":[]}
|
||||
LOG [API] getAllBeneficiaries - token exists: true length: 187
|
||||
LOG [API] getAllBeneficiaries - Fetching from: https://wellnuo.smartlaunchhub.com/api/me/beneficiaries
|
||||
LOG [API] getAllBeneficiaries - Response status: 200
|
||||
LOG [API] getAllBeneficiaries - Data: {"beneficiaries":[]}
|
||||
LOG [API] getAllBeneficiaries - token exists: true length: 187
|
||||
LOG [API] getAllBeneficiaries - Fetching from: https://wellnuo.smartlaunchhub.com/api/me/beneficiaries
|
||||
LOG [API] getAllBeneficiaries - Response status: 200
|
||||
LOG [API] getAllBeneficiaries - Data: {"beneficiaries":[{"accessId":41,"id":21,"role":"custodian","grantedAt":"2026-01-09T04:15:44.403Z","name":"Mama","phone":null,"address":null,"createdAt":"2026-01-09T04:15:43.079Z","subscription":{"pla
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":21,"name":"Mama","hasDevices":false,"equipmentStatus":"none"}
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":21,"name":"Mama","hasDevices":false,"equipmentStatus":"none"}
|
||||
LOG [API] getAllBeneficiaries - token exists: true length: 187
|
||||
LOG [API] getAllBeneficiaries - Fetching from: https://wellnuo.smartlaunchhub.com/api/me/beneficiaries
|
||||
LOG [API] getAllBeneficiaries - Response status: 200
|
||||
LOG [API] getAllBeneficiaries - Data: {"beneficiaries":[{"accessId":41,"id":21,"role":"custodian","grantedAt":"2026-01-09T04:15:44.403Z","name":"Mama","phone":null,"address":null,"createdAt":"2026-01-09T04:15:43.079Z","subscription":{"pla
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":23,"name":"Mama3","hasDevices":false,"equipmentStatus":"none"}
|
||||
LOG [Purchase] Creating payment sheet for userId: 62 beneficiaryId: 23
|
||||
LOG [Purchase] Payment successful, updating equipment status...
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":23,"name":"Mama3","hasDevices":false,"equipmentStatus":"ordered"}
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":23,"name":"Mama3","hasDevices":true,"equipmentStatus":"demo"}
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":23,"name":"Mama3","hasDevices":true,"equipmentStatus":"demo"}
|
||||
ERROR Payment error: [Error: Failed to create subscription]
|
||||
|
||||
Code: subscription.tsx
|
||||
[0m [90m 143 |[39m
|
||||
[90m 144 |[39m [36mif[39m ([33m![39mdata[33m.[39mclientSecret) {
|
||||
[31m[1m>[22m[39m[90m 145 |[39m [36mthrow[39m [36mnew[39m [33mError[39m(data[33m.[39merror [33m||[39m [32m'Failed to create subscription'[39m)[33m;[39m
|
||||
[90m |[39m [31m[1m^[22m[39m
|
||||
[90m 146 |[39m }
|
||||
[90m 147 |[39m
|
||||
[90m 148 |[39m [90m// 2. Initialize the Payment Sheet[39m[0m
|
||||
Call Stack
|
||||
handleSubscribe (app/(tabs)/beneficiaries/[id]/subscription.tsx:145:24)
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":23,"name":"Mama3","hasDevices":true,"equipmentStatus":"demo"}
|
||||
LOG [DEBUG] loadBeneficiary: fetching id=23
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":23,"name":"Mama3","hasDevices":true,"equipmentStatus":"demo"}
|
||||
LOG [DEBUG] loadBeneficiary: response.ok=true
|
||||
LOG [DEBUG] loadBeneficiary: got beneficiary id=23, name=Mama3
|
||||
LOG [DEBUG] loadBeneficiary: fetching id=23
|
||||
LOG [API] getWellNuoBeneficiary - Raw response: {"id":23,"name":"Mama3","hasDevices":true,"equipmentStatus":"demo"}
|
||||
LOG [DEBUG] loadBeneficiary: response.ok=true
|
||||
LOG [DEBUG] loadBeneficiary: got beneficiary id=23, name=Mama3
|
||||
LOG [DEBUG] handleSubscribe: START
|
||||
LOG [DEBUG] handleSubscribe: beneficiary={"id":23,"name":"Mama3"}
|
||||
LOG [DEBUG] handleSubscribe: token=eyJhbGciOiJIUzI1NiIs...
|
||||
LOG [DEBUG] handleSubscribe: calling https://wellnuo.smartlaunchhub.com/api/stripe/create-subscription-payment-sheet
|
||||
LOG [DEBUG] handleSubscribe: body={"beneficiaryId":23}
|
||||
LOG [DEBUG] handleSubscribe: response.status=200
|
||||
LOG [DEBUG] handleSubscribe: response data={"subscriptionId":"sub_1SnYNwP0gvUw6M9CMooRyBOz","ephemeralKey":"ek_test_YWNjdF8xUDNrZHFQMGd2VXc2TTlDLGlmOGdHVldBc1ppNkxxZmM1N1B2WXJuZWNSZ1lRbkg_00BcXeNVrC","customer":"cus_Tl4QHSfVqjstea","publishableKey":"pk_test_51P3kdqP0gvUw6M9C7ixPQHqbPcvga4G5kAYx1h6QXQAt1psbrC2rrmOojW0fTeQzaxD1Q9RKS3zZ23MCvjjZpWLi00eCFWRHMk"}
|
||||
ERROR Payment error: [Error: Failed to create subscription]
|
||||
|
||||
Code: subscription.tsx
|
||||
[0m [90m 180 |[39m
|
||||
[90m 181 |[39m [36mif[39m ([33m![39mdata[33m.[39mclientSecret) {
|
||||
[31m[1m>[22m[39m[90m 182 |[39m [36mthrow[39m [36mnew[39m [33mError[39m(data[33m.[39merror [33m||[39m [32m'Failed to create subscription'[39m)[33m;[39m
|
||||
[90m |[39m [31m[1m^[22m[39m
|
||||
[90m 183 |[39m }
|
||||
[90m 184 |[39m
|
||||
[90m 185 |[39m [90m// 2. Initialize the Payment Sheet[39m[0m
|
||||
Call Stack
|
||||
handleSubscribe (app/(tabs)/beneficiaries/[id]/subscription.tsx:182:24)
|
||||
Unable to resolve "react-native-root-toast" from "app/(tabs)/beneficiaries/[id]/subscription.tsx"
|
||||
[0m [90m 21 |[39m [36mimport[39m type { [33mBeneficiary[39m } [36mfrom[39m [32m'@/types'[39m[33m;[39m
|
||||
[90m 22 |[39m [36mimport[39m [33m*[39m [36mas[39m [33mExpoClipboard[39m [36mfrom[39m [32m'expo-clipboard'[39m[33m;[39m
|
||||
[31m[1m>[22m[39m[90m 23 |[39m [36mimport[39m [33mToast[39m [36mfrom[39m [32m'react-native-root-toast'[39m[33m;[39m
|
||||
[90m |[39m [31m[1m^[22m[39m
|
||||
[90m 24 |[39m
|
||||
[90m 25 |[39m [36mconst[39m [33mSTRIPE_API_URL[39m [33m=[39m [32m'https://wellnuo.smartlaunchhub.com/api/stripe'[39m[33m;[39m
|
||||
[90m 26 |[39m [36mconst[39m [33mSUBSCRIPTION_PRICE[39m [33m=[39m [35m49[39m[33m;[39m [90m// $49/month[39m[0m
|
||||
|
||||
Import stack:
|
||||
|
||||
app/(tabs)/beneficiaries/[id]/subscription.tsx
|
||||
| import "react-native-root-toast"
|
||||
|
||||
app (require.context)
|
||||
|
||||
Unable to resolve "react-native-root-toast" from "app/(tabs)/beneficiaries/[id]/subscription.tsx"
|
||||
[0m [90m 21 |[39m [36mimport[39m type { [33mBeneficiary[39m } [36mfrom[39m [32m'@/types'[39m[33m;[39m
|
||||
[90m 22 |[39m [36mimport[39m [33m*[39m [36mas[39m [33mExpoClipboard[39m [36mfrom[39m [32m'expo-clipboard'[39m[33m;[39m
|
||||
[31m[1m>[22m[39m[90m 23 |[39m [36mimport[39m [33mToast[39m [36mfrom[39m [32m'react-native-root-toast'[39m[33m;[39m
|
||||
[90m |[39m [31m[1m^[22m[39m
|
||||
[90m 24 |[39m
|
||||
[90m 25 |[39m [36mconst[39m [33mSTRIPE_API_URL[39m [33m=[39m [32m'https://wellnuo.smartlaunchhub.com/api/stripe'[39m[33m;[39m
|
||||
[90m 26 |[39m [36mconst[39m [33mSUBSCRIPTION_PRICE[39m [33m=[39m [35m49[39m[33m;[39m [90m// $49/month[39m[0m
|
||||
|
||||
Import stack:
|
||||
|
||||
app/(tabs)/beneficiaries/[id]/subscription.tsx
|
||||
| import "react-native-root-toast"
|
||||
|
||||
app (require.context)
|
||||
|
||||
Unable to resolve "react-native-root-toast" from "app/(tabs)/beneficiaries/[id]/subscription.tsx"
|
||||
[0m [90m 21 |[39m [36mimport[39m type { [33mBeneficiary[39m } [36mfrom[39m [32m'@/types'[39m[33m;[39m
|
||||
[90m 22 |[39m [36mimport[39m [33m*[39m [36mas[39m [33mExpoClipboard[39m [36mfrom[39m [32m'expo-clipboard'[39m[33m;[39m
|
||||
[31m[1m>[22m[39m[90m 23 |[39m [36mimport[39m [33mToast[39m [36mfrom[39m [32m'react-native-root-toast'[39m[33m;[39m
|
||||
[90m |[39m [31m[1m^[22m[39m
|
||||
[90m 24 |[39m
|
||||
[90m 25 |[39m [36mconst[39m [33mSTRIPE_API_URL[39m [33m=[39m [32m'https://wellnuo.smartlaunchhub.com/api/stripe'[39m[33m;[39m
|
||||
[90m 26 |[39m [36mconst[39m [33mSUBSCRIPTION_PRICE[39m [33m=[39m [35m49[39m[33m;[39m [90m// $49/month[39m[0m
|
||||
|
||||
Import stack:
|
||||
|
||||
app/(tabs)/beneficiaries/[id]/subscription.tsx
|
||||
| import "react-native-root-toast"
|
||||
|
||||
app (require.context)
|
||||
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@ -22,7 +22,6 @@
|
||||
"expo-clipboard": "~8.0.8",
|
||||
"expo-constants": "~18.0.12",
|
||||
"expo-crypto": "~15.0.8",
|
||||
"expo-file-system": "~19.0.21",
|
||||
"expo-font": "~14.0.10",
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
|
||||
@ -25,7 +25,6 @@
|
||||
"expo-clipboard": "~8.0.8",
|
||||
"expo-constants": "~18.0.12",
|
||||
"expo-crypto": "~15.0.8",
|
||||
"expo-file-system": "~19.0.21",
|
||||
"expo-font": "~14.0.10",
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
|
||||
254
services/api.ts
254
services/api.ts
@ -1,6 +1,5 @@
|
||||
import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings } from '@/types';
|
||||
import * as Crypto from 'expo-crypto';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
|
||||
// Callback for handling unauthorized responses (401)
|
||||
@ -610,10 +609,10 @@ class ApiService {
|
||||
const beneficiaries: Beneficiary[] = (data.beneficiaries || []).map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name || item.email,
|
||||
avatar: item.avatarUrl || undefined, // Use uploaded avatar from server
|
||||
avatar: undefined, // No auto-generated avatars - only show if user uploaded one
|
||||
status: 'offline' as const,
|
||||
email: item.email,
|
||||
address: typeof item.address === 'string' ? item.address : (item.address?.street ? `${item.address.street}, ${item.address.city}` : undefined),
|
||||
address: item.address?.street ? `${item.address.street}, ${item.address.city}` : undefined,
|
||||
subscription: item.subscription,
|
||||
// Equipment status from orders
|
||||
equipmentStatus: item.equipmentStatus,
|
||||
@ -650,8 +649,7 @@ class ApiService {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
hasDevices: data.hasDevices,
|
||||
equipmentStatus: data.equipmentStatus,
|
||||
subscription: data.subscription
|
||||
equipmentStatus: data.equipmentStatus
|
||||
}));
|
||||
|
||||
if (!response.ok) {
|
||||
@ -661,15 +659,14 @@ class ApiService {
|
||||
const beneficiary: Beneficiary = {
|
||||
id: data.id,
|
||||
name: data.name || data.email,
|
||||
avatar: data.avatarUrl || undefined,
|
||||
avatar: undefined, // No auto-generated avatars - only show if user uploaded one
|
||||
status: 'offline' as const,
|
||||
email: data.email,
|
||||
address: typeof data.address === 'string' ? data.address : (data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined),
|
||||
address: data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined,
|
||||
subscription: data.subscription ? {
|
||||
status: data.subscription.status,
|
||||
plan: data.subscription.plan,
|
||||
endDate: data.subscription.endDate,
|
||||
cancelAtPeriodEnd: data.subscription.cancelAtPeriodEnd,
|
||||
endDate: data.subscription.currentPeriodEnd,
|
||||
} : undefined,
|
||||
// Equipment status from orders
|
||||
equipmentStatus: data.equipmentStatus,
|
||||
@ -764,59 +761,6 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// Upload/update beneficiary avatar
|
||||
async updateBeneficiaryAvatar(id: number, imageUri: string | null): Promise<ApiResponse<{ avatarUrl: string | null }>> {
|
||||
const token = await this.getToken();
|
||||
|
||||
if (!token) {
|
||||
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
|
||||
}
|
||||
|
||||
try {
|
||||
let base64Image: string | null = null;
|
||||
|
||||
if (imageUri) {
|
||||
// Read file as base64 string using expo-file-system
|
||||
const base64Data = await FileSystem.readAsStringAsync(imageUri, {
|
||||
encoding: 'base64',
|
||||
});
|
||||
|
||||
// Determine mime type from URI extension
|
||||
const extension = imageUri.split('.').pop()?.toLowerCase() || 'jpeg';
|
||||
const mimeType = extension === 'png' ? 'image/png' : 'image/jpeg';
|
||||
|
||||
// Create data URI
|
||||
base64Image = `data:${mimeType};base64,${base64Data}`;
|
||||
|
||||
console.log('[API] Avatar converted to base64, length:', base64Image.length);
|
||||
}
|
||||
|
||||
console.log('[API] Uploading avatar for beneficiary:', id);
|
||||
|
||||
const apiResponse = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}/avatar`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ avatar: base64Image }),
|
||||
});
|
||||
|
||||
const data = await apiResponse.json();
|
||||
|
||||
console.log('[API] Avatar upload response:', apiResponse.status, data);
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
return { ok: false, error: { message: data.error || 'Failed to update avatar' } };
|
||||
}
|
||||
|
||||
return { data: { avatarUrl: data.beneficiary?.avatarUrl || null }, ok: true };
|
||||
} catch (error) {
|
||||
console.error('[API] updateBeneficiaryAvatar error:', error);
|
||||
return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } };
|
||||
}
|
||||
}
|
||||
|
||||
// Delete beneficiary (removes access record)
|
||||
async deleteBeneficiary(id: number): Promise<ApiResponse<{ success: boolean }>> {
|
||||
const token = await this.getToken();
|
||||
@ -1103,53 +1047,6 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// Get transaction history from Stripe
|
||||
async getTransactionHistory(beneficiaryId: number, limit = 10): Promise<ApiResponse<{
|
||||
transactions: Array<{
|
||||
id: string;
|
||||
type: 'subscription' | 'one_time';
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: string;
|
||||
date: string;
|
||||
description: string;
|
||||
invoicePdf?: string;
|
||||
hostedUrl?: string;
|
||||
receiptUrl?: string;
|
||||
}>;
|
||||
hasMore: boolean;
|
||||
}>> {
|
||||
const token = await this.getToken();
|
||||
if (!token) {
|
||||
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${WELLNUO_API_URL}/stripe/transaction-history/${beneficiaryId}?limit=${limit}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
return { data, ok: true };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: data.error || 'Failed to get transaction history' },
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: 'Network error. Please check your connection.' },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Reactivate subscription that was set to cancel
|
||||
async reactivateSubscription(beneficiaryId: number): Promise<ApiResponse<{ success: boolean; status: string }>> {
|
||||
const token = await this.getToken();
|
||||
@ -1289,145 +1186,6 @@ class ApiService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Legacy Dashboard Methods (Developer Mode)
|
||||
// For eluxnetworks.net dashboard WebView
|
||||
// ==========================================
|
||||
|
||||
// Demo credentials for legacy dashboard
|
||||
private readonly DEMO_LEGACY_USER = 'anandk';
|
||||
private readonly DEMO_LEGACY_PASSWORD = 'anandk_8';
|
||||
private readonly DEMO_DEPLOYMENT_ID = 21; // Ferdinand's deployment
|
||||
|
||||
// Login to legacy dashboard API
|
||||
async loginToLegacyDashboard(): Promise<ApiResponse<AuthResponse>> {
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('function', 'credentials');
|
||||
formData.append('user_name', this.DEMO_LEGACY_USER);
|
||||
formData.append('ps', this.DEMO_LEGACY_PASSWORD);
|
||||
formData.append('clientId', CLIENT_ID);
|
||||
formData.append('nonce', this.generateNonce());
|
||||
|
||||
const response = await fetch(API_BASE_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData.toString(),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('[API] Legacy login response:', data.status, 'token type:', typeof data.access_token);
|
||||
|
||||
// Check that access_token is a valid JWT string (not 0 or empty)
|
||||
if (data.status === '200 OK' && data.access_token && typeof data.access_token === 'string' && data.access_token.includes('.')) {
|
||||
// Save legacy credentials
|
||||
await SecureStore.setItemAsync('legacyAccessToken', data.access_token);
|
||||
await SecureStore.setItemAsync('legacyUserId', String(data.user_id));
|
||||
await SecureStore.setItemAsync('legacyUserName', this.DEMO_LEGACY_USER);
|
||||
console.log('[API] Legacy credentials saved successfully');
|
||||
|
||||
return { data: data as AuthResponse, ok: true };
|
||||
}
|
||||
|
||||
console.log('[API] Legacy login failed - invalid token:', data.access_token);
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: data.message || 'Legacy login failed - invalid credentials' },
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Legacy login error:', error);
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: 'Failed to connect to dashboard API' },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh legacy token
|
||||
async refreshLegacyToken(): Promise<ApiResponse<AuthResponse>> {
|
||||
console.log('[API] Refreshing legacy token...');
|
||||
return this.loginToLegacyDashboard();
|
||||
}
|
||||
|
||||
// Check if legacy token is expiring soon (within 1 hour)
|
||||
async isLegacyTokenExpiringSoon(): Promise<boolean> {
|
||||
try {
|
||||
const token = await this.getLegacyToken();
|
||||
if (!token) return true;
|
||||
|
||||
// Decode JWT to get expiration
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return true;
|
||||
|
||||
const payload = JSON.parse(atob(parts[1]));
|
||||
const exp = payload.exp;
|
||||
if (!exp) return true;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const oneHour = 60 * 60;
|
||||
|
||||
const isExpiring = (exp - now) < oneHour;
|
||||
console.log('[API] Legacy token expiring soon:', isExpiring, 'expires in:', Math.round((exp - now) / 60), 'min');
|
||||
return isExpiring;
|
||||
} catch (e) {
|
||||
console.log('[API] Error checking legacy token:', e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Get legacy credentials for WebView injection
|
||||
async getLegacyWebViewCredentials(): Promise<{
|
||||
token: string;
|
||||
userName: string;
|
||||
userId: string;
|
||||
} | null> {
|
||||
try {
|
||||
const token = await SecureStore.getItemAsync('legacyAccessToken');
|
||||
const userName = await SecureStore.getItemAsync('legacyUserName');
|
||||
const userId = await SecureStore.getItemAsync('legacyUserId');
|
||||
|
||||
// Check if credentials exist AND token is valid JWT (contains dots)
|
||||
const isValidToken = token && typeof token === 'string' && token.includes('.');
|
||||
|
||||
if (!isValidToken || !userName || !userId) {
|
||||
console.log('[API] Legacy credentials missing or invalid token, logging in...');
|
||||
console.log('[API] Token valid:', isValidToken, 'userName:', !!userName, 'userId:', !!userId);
|
||||
|
||||
// Clear any invalid cached credentials
|
||||
if (token && !isValidToken) {
|
||||
console.log('[API] Clearing invalid cached token:', token);
|
||||
await SecureStore.deleteItemAsync('legacyAccessToken');
|
||||
await SecureStore.deleteItemAsync('legacyUserName');
|
||||
await SecureStore.deleteItemAsync('legacyUserId');
|
||||
}
|
||||
|
||||
const loginResult = await this.loginToLegacyDashboard();
|
||||
if (!loginResult.ok) return null;
|
||||
|
||||
// Get freshly saved credentials
|
||||
const newToken = await SecureStore.getItemAsync('legacyAccessToken');
|
||||
const newUserName = await SecureStore.getItemAsync('legacyUserName');
|
||||
const newUserId = await SecureStore.getItemAsync('legacyUserId');
|
||||
|
||||
if (!newToken || !newUserName || !newUserId) return null;
|
||||
return { token: newToken, userName: newUserName, userId: newUserId };
|
||||
}
|
||||
|
||||
console.log('[API] Legacy credentials found:', userName, 'token length:', token.length);
|
||||
return { token, userName, userId };
|
||||
} catch (e) {
|
||||
console.error('[API] Error getting legacy credentials:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get demo deployment ID
|
||||
getDemoDeploymentId(): number {
|
||||
return this.DEMO_DEPLOYMENT_ID;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ApiService();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user