- 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
209 lines
5.8 KiB
TypeScript
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);
|
|
}
|