WellNuo/services/pushNotifications.ts
Sergei 671374da9a Improve BLE WiFi error handling and logging
- setWiFi() now throws detailed errors instead of returning false
- Shows specific error messages: "WiFi credentials rejected", timeout etc.
- Added logging throughout BLE WiFi configuration flow
- Fixed WiFi network deduplication (keeps strongest signal)
- Ignore "Operation cancelled" error (normal cleanup behavior)
- BatchSetupProgress shows actual error in hint field

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 19:10:45 -08:00

248 lines
6.8 KiB
TypeScript

/**
* Push Notifications Service
*
* Handles:
* - Requesting push notification permissions
* - Getting Expo Push Token
* - Registering/unregistering token on server
* - Handling incoming notifications
*/
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import Constants from 'expo-constants';
const WELLNUO_API_URL = 'https://wellnuo.smartlaunchhub.com/api';
const PUSH_TOKEN_KEY = 'expoPushToken';
// Configure notification handler for foreground notifications
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
/**
* Register for push notifications and get Expo Push Token
* Returns the token or null if not available
*/
export async function registerForPushNotificationsAsync(): Promise<string | null> {
let token: string | null = null;
// Must be a physical device for push notifications
if (!Device.isDevice) {
console.log('[Push] Must use physical device for push notifications');
// For simulator, return a fake token for testing
if (__DEV__) {
return 'ExponentPushToken[SIMULATOR_TEST_TOKEN]';
}
return null;
}
// Check/request permissions
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('[Push] Permission not granted');
return null;
}
// Get Expo Push Token
try {
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const pushTokenResponse = await Notifications.getExpoPushTokenAsync({
projectId: projectId,
});
token = pushTokenResponse.data;
console.log('[Push] Got Expo Push Token:', token);
// Store locally
await SecureStore.setItemAsync(PUSH_TOKEN_KEY, token);
} catch (error) {
console.error('[Push] Error getting push token:', error);
return null;
}
// Android needs notification channel
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
// Emergency alerts channel
await Notifications.setNotificationChannelAsync('emergency', {
name: 'Emergency Alerts',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 500, 200, 500],
lightColor: '#FF0000',
sound: 'default',
});
}
return token;
}
/**
* Register push token on WellNuo backend
*/
export async function registerTokenOnServer(token: string): Promise<boolean> {
try {
const accessToken = await SecureStore.getItemAsync('accessToken');
if (!accessToken) {
console.log('[Push] No access token, skipping server registration');
return false;
}
const response = await fetch(`${WELLNUO_API_URL}/push-tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({
token,
platform: Platform.OS,
deviceName: Device.deviceName || `${Device.brand} ${Device.modelName}`,
}),
});
if (!response.ok) {
const error = await response.json();
console.error('[Push] Server registration failed:', error);
return false;
}
console.log('[Push] Token registered on server successfully');
return true;
} catch (error) {
console.error('[Push] Error registering token on server:', error);
return false;
}
}
/**
* Unregister push token from server (call on logout)
*/
export async function unregisterToken(): Promise<boolean> {
try {
const token = await SecureStore.getItemAsync(PUSH_TOKEN_KEY);
const accessToken = await SecureStore.getItemAsync('accessToken');
if (!token || !accessToken) {
return true; // Nothing to unregister
}
const response = await fetch(`${WELLNUO_API_URL}/push-tokens`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({ token }),
});
// Clear local storage regardless of server response
await SecureStore.deleteItemAsync(PUSH_TOKEN_KEY);
if (!response.ok) {
console.error('[Push] Server unregistration failed');
return false;
}
console.log('[Push] Token unregistered successfully');
return true;
} catch (error) {
console.error('[Push] Error unregistering token:', error);
// Still clear local storage
await SecureStore.deleteItemAsync(PUSH_TOKEN_KEY);
return false;
}
}
/**
* Full registration flow: get token + register on server
* Call this after successful login
*/
export async function setupPushNotifications(): Promise<string | null> {
console.log('[Push] Setting up push notifications...');
const token = await registerForPushNotificationsAsync();
if (token) {
await registerTokenOnServer(token);
}
return token;
}
/**
* Get stored push token (if any)
*/
export async function getStoredPushToken(): Promise<string | null> {
try {
return await SecureStore.getItemAsync(PUSH_TOKEN_KEY);
} catch {
return null;
}
}
/**
* Add listener for received notifications (while app is open)
*/
export function addNotificationReceivedListener(
callback: (notification: Notifications.Notification) => void
): Notifications.EventSubscription {
return Notifications.addNotificationReceivedListener(callback);
}
/**
* Add listener for notification response (user tapped notification)
*/
export function addNotificationResponseListener(
callback: (response: Notifications.NotificationResponse) => void
): Notifications.EventSubscription {
return Notifications.addNotificationResponseReceivedListener(callback);
}
/**
* Get last notification response (if app was opened from notification)
*/
export async function getLastNotificationResponse(): Promise<Notifications.NotificationResponse | null> {
return await Notifications.getLastNotificationResponseAsync();
}
/**
* Parse notification data to determine navigation target
*/
export function parseNotificationData(data: Record<string, unknown>): {
type: string;
deploymentId?: number;
beneficiaryId?: number;
alertId?: string;
} {
return {
type: (data.type as string) || 'unknown',
deploymentId: data.deploymentId as number | undefined,
beneficiaryId: data.beneficiaryId as number | undefined,
alertId: data.alertId as string | undefined,
};
}