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:
parent
ccf1701a34
commit
5737dce4ba
@ -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
|
1. BACKEND/API DEVELOPMENT ACCESS
|
||||||
|
|
||||||
Request: Can I get access to develop backend/API endpoints myself?
|
Request: Can I get access to develop backend/API endpoints myself?
|
||||||
@ -19,7 +31,7 @@ Option A: Access Granted (Preferred)
|
|||||||
|
|
||||||
I need:
|
I need:
|
||||||
- Repository access (or separate repo)
|
- Repository access (or separate repo)
|
||||||
- Database documentation
|
- Database access OR database dump (so I can set up my own)
|
||||||
- Architecture overview
|
- Architecture overview
|
||||||
|
|
||||||
I will:
|
I will:
|
||||||
@ -57,6 +69,17 @@ Hide:
|
|||||||
Reason: Mobile app has its own navigation and auth.
|
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
|
SUMMARY
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export default function LoginScreen() {
|
|||||||
const success = await login({ username: username.trim(), password });
|
const success = await login({ username: username.trim(), password });
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
// Clear password from memory after successful login
|
||||||
|
setPassword('');
|
||||||
router.replace('/(tabs)');
|
router.replace('/(tabs)');
|
||||||
}
|
}
|
||||||
}, [username, password, login, clearError]);
|
}, [username, password, login, clearError]);
|
||||||
|
|||||||
@ -30,11 +30,30 @@ export default function ChatScreen() {
|
|||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
const flatListRef = useRef<FlatList>(null);
|
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 handleSend = useCallback(async () => {
|
||||||
const trimmedInput = input.trim();
|
const trimmedInput = input.trim();
|
||||||
if (!trimmedInput || isSending) return;
|
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 = {
|
const userMessage: Message = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@ -53,8 +72,8 @@ export default function ChatScreen() {
|
|||||||
? `${beneficiaryContext} ${trimmedInput}`
|
? `${beneficiaryContext} ${trimmedInput}`
|
||||||
: trimmedInput;
|
: trimmedInput;
|
||||||
|
|
||||||
// Pass deployment_id from selected beneficiary (fallback to '21' if not selected)
|
// Pass deployment_id from selected beneficiary (required, no fallback)
|
||||||
const deploymentId = currentBeneficiary?.id?.toString() || '21';
|
const deploymentId = currentBeneficiary.id.toString();
|
||||||
const response = await api.sendMessage(questionWithContext, deploymentId);
|
const response = await api.sendMessage(questionWithContext, deploymentId);
|
||||||
|
|
||||||
if (response.ok && response.data?.response) {
|
if (response.ok && response.data?.response) {
|
||||||
@ -85,7 +104,7 @@ export default function ChatScreen() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsSending(false);
|
setIsSending(false);
|
||||||
}
|
}
|
||||||
}, [input, isSending, getBeneficiaryContext]);
|
}, [input, isSending, currentBeneficiary, getBeneficiaryContext]);
|
||||||
|
|
||||||
const renderMessage = ({ item }: { item: Message }) => {
|
const renderMessage = ({ item }: { item: Message }) => {
|
||||||
const isUser = item.role === 'user';
|
const isUser = item.role === 'user';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from 'react';
|
||||||
import { api } from '@/services/api';
|
import { api, setOnUnauthorizedCallback } from '@/services/api';
|
||||||
import type { User, LoginCredentials, ApiError } from '@/types';
|
import type { User, LoginCredentials, ApiError } from '@/types';
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
@ -30,6 +30,20 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
checkAuth();
|
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 () => {
|
const checkAuth = async () => {
|
||||||
try {
|
try {
|
||||||
const isAuth = await api.isAuthenticated();
|
const isAuth = await api.isAuthenticated();
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"expo": "~54.0.29",
|
"expo": "~54.0.29",
|
||||||
"expo-constants": "~18.0.12",
|
"expo-constants": "~18.0.12",
|
||||||
|
"expo-crypto": "~15.0.8",
|
||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
@ -6581,6 +6582,18 @@
|
|||||||
"react-native": "*"
|
"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": {
|
"node_modules/expo-file-system": {
|
||||||
"version": "19.0.21",
|
"version": "19.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"expo": "~54.0.29",
|
"expo": "~54.0.29",
|
||||||
"expo-constants": "~18.0.12",
|
"expo-constants": "~18.0.12",
|
||||||
|
"expo-crypto": "~15.0.8",
|
||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import * as Crypto from 'expo-crypto';
|
||||||
import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, DashboardSingleResponse, PatientDashboardData } from '@/types';
|
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 API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
|
||||||
const CLIENT_ID = 'MA_001';
|
const CLIENT_ID = 'MA_001';
|
||||||
|
|
||||||
@ -51,7 +59,10 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private generateNonce(): string {
|
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>> {
|
private async makeRequest<T>(params: Record<string, string>): Promise<ApiResponse<T>> {
|
||||||
@ -71,6 +82,21 @@ class ApiService {
|
|||||||
|
|
||||||
const data = await response.json();
|
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) {
|
if (data.status === '200 OK' || data.ok === true) {
|
||||||
return { data: data as T, ok: true };
|
return { data: data as T, ok: true };
|
||||||
}
|
}
|
||||||
@ -309,8 +335,11 @@ class ApiService {
|
|||||||
return { data: patients, ok: true };
|
return { data: patients, ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI Chat
|
// AI Chat - deploymentId is required, no default value for security
|
||||||
async sendMessage(question: string, deploymentId: string = '21'): Promise<ApiResponse<ChatResponse>> {
|
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 token = await this.getToken();
|
||||||
const userName = await this.getUserName();
|
const userName = await this.getUserName();
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,18 @@
|
|||||||
"x": 520,
|
"x": 520,
|
||||||
"y": 300
|
"y": 300
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "q_account_access",
|
||||||
|
"type": "card",
|
||||||
|
"title": "Account Access",
|
||||||
|
"borderColor": "yellow",
|
||||||
|
"tags": [
|
||||||
|
"integration"
|
||||||
|
],
|
||||||
|
"description": "Need access to:\n\n1. Apple Developer Account\n - App Store submissions\n - Certificates & provisioning\n - Push notifications setup\n\n2. Stripe Account (if using payments)\n - API keys\n - Products & prices config\n - Webhooks setup",
|
||||||
|
"x": 520,
|
||||||
|
"y": 480
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api_yes",
|
"id": "api_yes",
|
||||||
"type": "card",
|
"type": "card",
|
||||||
@ -190,6 +202,10 @@
|
|||||||
"from": "header",
|
"from": "header",
|
||||||
"to": "q_api_access"
|
"to": "q_api_access"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"from": "header",
|
||||||
|
"to": "q_account_access"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"from": "q_api_access",
|
"from": "q_api_access",
|
||||||
"to": "api_yes",
|
"to": "api_yes",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user