Sync latest changes

- Updated login screen
- Updated chat screen
- Updated AuthContext
- Updated API service
- Updated packages
- Updated discussion docs and scheme

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2025-12-12 17:14:45 -08:00
parent 1e2c2b9856
commit 8590202d53
7 changed files with 110 additions and 9 deletions

View File

@ -3,6 +3,18 @@ Discussion Points for EluxNetworks Team
────────────────────────────────────────────────────────────
CURRENT STATUS
I've uploaded the latest build to TestFlight. Please test on iOS.
Tomorrow I'll start working on Android deployment.
We need to schedule a call to discuss these questions in more detail.
I've hit some bottlenecks and need clarification before moving forward.
All questions are listed below and in the project scheme.
────────────────────────────────────────────────────────────
1. BACKEND/API DEVELOPMENT ACCESS
Request: Can I get access to develop backend/API endpoints myself?
@ -19,7 +31,7 @@ Option A: Access Granted (Preferred)
I need:
- Repository access (or separate repo)
- Database documentation
- Database access OR database dump (so I can set up my own)
- Architecture overview
I will:
@ -57,6 +69,17 @@ Hide:
Reason: Mobile app has its own navigation and auth.
────────────────────────────────────────────────────────────
3. ACCOUNT ACCESS NEEDED
Apple Developer Account
- Need access for App Store submissions, certificates, push notifications setup
Stripe Account (if we're integrating payments/subscriptions)
- Need API keys for payment integration
- Need access to configure products, prices, webhooks
────────────────────────────────────────────────────────────
SUMMARY

View File

@ -40,6 +40,8 @@ export default function LoginScreen() {
const success = await login({ username: username.trim(), password });
if (success) {
// Clear password from memory after successful login
setPassword('');
router.replace('/(tabs)');
}
}, [username, password, login, clearError]);

View File

@ -30,11 +30,30 @@ export default function ChatScreen() {
const [input, setInput] = useState('');
const [isSending, setIsSending] = useState(false);
const flatListRef = useRef<FlatList>(null);
const lastSendTimeRef = useRef<number>(0);
const SEND_COOLDOWN_MS = 1000; // 1 second cooldown between messages
const handleSend = useCallback(async () => {
const trimmedInput = input.trim();
if (!trimmedInput || isSending) return;
// Debounce: prevent rapid-fire messages
const now = Date.now();
if (now - lastSendTimeRef.current < SEND_COOLDOWN_MS) {
return;
}
lastSendTimeRef.current = now;
// Security: require beneficiary to be selected
if (!currentBeneficiary?.id) {
Alert.alert(
'Select Patient',
'Please select a patient from the Patients tab before starting a conversation.',
[{ text: 'OK' }]
);
return;
}
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
@ -53,8 +72,8 @@ export default function ChatScreen() {
? `${beneficiaryContext} ${trimmedInput}`
: trimmedInput;
// Pass deployment_id from selected beneficiary (fallback to '21' if not selected)
const deploymentId = currentBeneficiary?.id?.toString() || '21';
// Pass deployment_id from selected beneficiary (required, no fallback)
const deploymentId = currentBeneficiary.id.toString();
const response = await api.sendMessage(questionWithContext, deploymentId);
if (response.ok && response.data?.response) {
@ -85,7 +104,7 @@ export default function ChatScreen() {
} finally {
setIsSending(false);
}
}, [input, isSending, getBeneficiaryContext]);
}, [input, isSending, currentBeneficiary, getBeneficiaryContext]);
const renderMessage = ({ item }: { item: Message }) => {
const isUser = item.role === 'user';

View File

@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
import { api } from '@/services/api';
import React, { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from 'react';
import { api, setOnUnauthorizedCallback } from '@/services/api';
import type { User, LoginCredentials, ApiError } from '@/types';
interface AuthState {
@ -30,6 +30,20 @@ export function AuthProvider({ children }: { children: ReactNode }) {
checkAuth();
}, []);
// Set up callback for 401 responses - auto logout
useEffect(() => {
setOnUnauthorizedCallback(() => {
api.logout().then(() => {
setState({
user: null,
isLoading: false,
isAuthenticated: false,
error: { message: 'Session expired. Please login again.' },
});
});
});
}, []);
const checkAuth = async () => {
try {
const isAuth = await api.isAuthenticated();

13
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@react-navigation/native": "^7.1.8",
"expo": "~54.0.29",
"expo-constants": "~18.0.12",
"expo-crypto": "~15.0.8",
"expo-font": "~14.0.10",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
@ -6581,6 +6582,18 @@
"react-native": "*"
}
},
"node_modules/expo-crypto": {
"version": "15.0.8",
"resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.8.tgz",
"integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-file-system": {
"version": "19.0.21",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",

View File

@ -17,6 +17,7 @@
"@react-navigation/native": "^7.1.8",
"expo": "~54.0.29",
"expo-constants": "~18.0.12",
"expo-crypto": "~15.0.8",
"expo-font": "~14.0.10",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",

View File

@ -1,6 +1,14 @@
import * as SecureStore from 'expo-secure-store';
import * as Crypto from 'expo-crypto';
import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, DashboardSingleResponse, PatientDashboardData } from '@/types';
// Callback for handling unauthorized responses (401)
let onUnauthorizedCallback: (() => void) | null = null;
export function setOnUnauthorizedCallback(callback: () => void) {
onUnauthorizedCallback = callback;
}
const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
const CLIENT_ID = 'MA_001';
@ -51,7 +59,10 @@ class ApiService {
}
private generateNonce(): string {
return Math.floor(Math.random() * 1000000).toString();
const randomBytes = Crypto.getRandomBytes(16);
return Array.from(randomBytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
private async makeRequest<T>(params: Record<string, string>): Promise<ApiResponse<T>> {
@ -71,6 +82,21 @@ class ApiService {
const data = await response.json();
// Handle 401 Unauthorized - trigger logout
if (response.status === 401 || data.status === '401' || data.error === 'Unauthorized') {
if (onUnauthorizedCallback) {
onUnauthorizedCallback();
}
return {
ok: false,
error: {
message: 'Session expired. Please login again.',
code: 'UNAUTHORIZED',
status: 401,
},
};
}
if (data.status === '200 OK' || data.ok === true) {
return { data: data as T, ok: true };
}
@ -309,8 +335,11 @@ class ApiService {
return { data: patients, ok: true };
}
// AI Chat
async sendMessage(question: string, deploymentId: string = '21'): Promise<ApiResponse<ChatResponse>> {
// AI Chat - deploymentId is required, no default value for security
async sendMessage(question: string, deploymentId: string): Promise<ApiResponse<ChatResponse>> {
if (!deploymentId) {
return { ok: false, error: { message: 'Please select a patient first', code: 'NO_PATIENT_SELECTED' } };
}
const token = await this.getToken();
const userName = await this.getUserName();