learning_ai_common_plat/packages/react-native-platform-sdk/src/auth/index.ts
Saravana Achu Mac e174335a9e fix(rn-platform-sdk): align providers with platform-service APIs
- Feature flags: GET /flags/poll legacy { flags } + optional userId
- Kill switch: GET /settings/kill-switch, map message to reason
- Broadcasts: GET /broadcasts, POST dismiss; map server message shape
- Surveys: GET /surveys/active; submit via start/response/complete
- Auth: register(); login/register bodies include productId
- Telemetry: map queued events to TelemetryEventSchema; RN Platform import
- prepare script runs tsc on install

Made-with: Cursor
2026-03-30 01:12:18 -07:00

209 lines
5.8 KiB
TypeScript

/**
* Auth module — React context + hook for authentication in React Native apps.
*/
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
import type { PlatformSDK } from '../core.js';
export interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
userId: string | null;
email: string | null;
error: string | null;
}
export interface AuthContextType extends AuthState {
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string, displayName: string) => Promise<void>;
loginWithGoogle: (idToken: string) => Promise<void>;
loginWithApple: (idToken: string) => Promise<void>;
logout: () => Promise<void>;
refreshSession: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function useAuth(): AuthContextType {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within an AuthProvider');
return ctx;
}
interface AuthProviderProps {
sdk: PlatformSDK;
children: React.ReactNode;
/** Called when tokens are received — persist to secure storage */
onTokens?: (access: string, refresh: string) => void;
/** Called on logout — clear secure storage */
onLogout?: () => void;
}
export function AuthProvider({
sdk,
children,
onTokens,
onLogout,
}: AuthProviderProps): React.JSX.Element {
const [state, setState] = useState<AuthState>({
isAuthenticated: false,
isLoading: true,
userId: null,
email: null,
error: null,
});
const handleTokenResponse = useCallback(
async (res: Response) => {
if (!res.ok) {
const body = (await res.json().catch(() => ({ message: 'Login failed' }))) as {
message?: string;
};
throw new Error(body.message ?? `HTTP ${res.status}`);
}
const data = (await res.json()) as {
accessToken?: string;
refreshToken?: string;
user?: { id?: string; email?: string };
};
if (data.accessToken && data.refreshToken) {
onTokens?.(data.accessToken, data.refreshToken);
}
setState({
isAuthenticated: true,
isLoading: false,
userId: data.user?.id ?? null,
email: data.user?.email ?? null,
error: null,
});
},
[onTokens]
);
const login = useCallback(
async (email: string, password: string) => {
setState(s => ({ ...s, isLoading: true, error: null }));
try {
const res = await sdk.fetch('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password, productId: sdk.config.productId }),
});
await handleTokenResponse(res);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Login failed';
setState(s => ({ ...s, isLoading: false, error: msg }));
}
},
[sdk, handleTokenResponse]
);
const register = useCallback(
async (email: string, password: string, displayName: string) => {
setState(s => ({ ...s, isLoading: true, error: null }));
try {
const res = await sdk.fetch('/auth/register', {
method: 'POST',
body: JSON.stringify({
email,
password,
displayName,
productId: sdk.config.productId,
}),
});
await handleTokenResponse(res);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Registration failed';
setState(s => ({ ...s, isLoading: false, error: msg }));
}
},
[sdk, handleTokenResponse]
);
const loginWithGoogle = useCallback(
async (idToken: string) => {
setState(s => ({ ...s, isLoading: true, error: null }));
try {
const res = await sdk.fetch('/auth/oauth/google', {
method: 'POST',
body: JSON.stringify({ idToken }),
});
await handleTokenResponse(res);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Google login failed';
setState(s => ({ ...s, isLoading: false, error: msg }));
}
},
[sdk, handleTokenResponse]
);
const loginWithApple = useCallback(
async (idToken: string) => {
setState(s => ({ ...s, isLoading: true, error: null }));
try {
const res = await sdk.fetch('/auth/oauth/apple', {
method: 'POST',
body: JSON.stringify({ idToken }),
});
await handleTokenResponse(res);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Apple login failed';
setState(s => ({ ...s, isLoading: false, error: msg }));
}
},
[sdk, handleTokenResponse]
);
const logout = useCallback(async () => {
try {
await sdk.fetch('/auth/logout', { method: 'POST' });
} catch {
/* best-effort */
}
onLogout?.();
setState({
isAuthenticated: false,
isLoading: false,
userId: null,
email: null,
error: null,
});
}, [sdk, onLogout]);
const refreshSession = useCallback(async () => {
setState(s => ({ ...s, isLoading: true }));
try {
const res = await sdk.fetch('/auth/me');
if (res.ok) {
const data = (await res.json()) as { id?: string; email?: string };
setState({
isAuthenticated: true,
isLoading: false,
userId: data.id ?? null,
email: data.email ?? null,
error: null,
});
} else {
setState(s => ({ ...s, isAuthenticated: false, isLoading: false }));
}
} catch {
setState(s => ({ ...s, isLoading: false }));
}
}, [sdk]);
useEffect(() => {
refreshSession();
}, [refreshSession]);
const value: AuthContextType = {
...state,
login,
register,
loginWithGoogle,
loginWithApple,
logout,
refreshSession,
};
return React.createElement(AuthContext.Provider, { value }, children);
}