- react-native-platform-sdk: add tsconfig.json + 13 source files (core, auth, telemetry, feature-flags, kill-switch, broadcasts, surveys) - react-native-platform-sdk: React hooks + providers wrapping platform-service APIs via fetch - admin-web: fix ThemeEditor.tsx + themes/active/route.ts lysnrai token type access - tracker-web: product-context import path fix (pre-existing)
105 lines
2.8 KiB
TypeScript
105 lines
2.8 KiB
TypeScript
/**
|
|
* Broadcasts module — React context + hook for in-app messages in React Native apps.
|
|
*/
|
|
|
|
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
|
import type { PlatformSDK } from '../core.js';
|
|
|
|
export interface InAppMessage {
|
|
id: string;
|
|
title: string;
|
|
body: string;
|
|
type: 'info' | 'warning' | 'critical';
|
|
action?: { label: string; url: string };
|
|
dismissible: boolean;
|
|
expiresAt?: string;
|
|
}
|
|
|
|
interface BroadcastContextType {
|
|
messages: InAppMessage[];
|
|
dismiss: (id: string) => void;
|
|
refresh: () => Promise<void>;
|
|
}
|
|
|
|
const BroadcastContext = createContext<BroadcastContextType | null>(null);
|
|
|
|
export function useBroadcasts(): BroadcastContextType {
|
|
const ctx = useContext(BroadcastContext);
|
|
if (!ctx) throw new Error('useBroadcasts must be used within a BroadcastProvider');
|
|
return ctx;
|
|
}
|
|
|
|
interface BroadcastProviderProps {
|
|
sdk: PlatformSDK;
|
|
/** Poll interval in ms (default: 300000 = 5 min) */
|
|
pollInterval?: number;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export function BroadcastProvider({
|
|
sdk,
|
|
pollInterval = 300_000,
|
|
children,
|
|
}: BroadcastProviderProps): React.JSX.Element {
|
|
const [messages, setMessages] = useState<InAppMessage[]>([]);
|
|
|
|
const refresh = useCallback(async () => {
|
|
try {
|
|
const res = await sdk.fetch('/api/broadcasts/active');
|
|
if (res.ok) {
|
|
const data = (await res.json()) as InAppMessage[];
|
|
setMessages(data);
|
|
}
|
|
} catch {
|
|
/* silent */
|
|
}
|
|
}, [sdk]);
|
|
|
|
const dismiss = useCallback(
|
|
(id: string) => {
|
|
setMessages(prev => prev.filter(m => m.id !== id));
|
|
// Fire-and-forget dismiss on server
|
|
sdk.fetch(`/api/broadcasts/${id}/dismiss`, { method: 'POST' }).catch(() => {});
|
|
},
|
|
[sdk]
|
|
);
|
|
|
|
useEffect(() => {
|
|
refresh();
|
|
const id = setInterval(refresh, pollInterval);
|
|
return () => clearInterval(id);
|
|
}, [refresh, pollInterval]);
|
|
|
|
const value: BroadcastContextType = { messages, dismiss, refresh };
|
|
return React.createElement(BroadcastContext.Provider, { value }, children);
|
|
}
|
|
|
|
// MARK: - UI Components
|
|
|
|
interface InAppMessageBannerProps {
|
|
message: InAppMessage;
|
|
onDismiss: () => void;
|
|
}
|
|
|
|
/**
|
|
* Placeholder banner component — product apps should implement their own
|
|
* styled version using this as a reference. Returns null (render-only hook).
|
|
*/
|
|
export function InAppMessageBanner(_props: InAppMessageBannerProps): React.JSX.Element | null {
|
|
// Product apps implement their own styled component
|
|
return null;
|
|
}
|
|
|
|
interface BroadcastModalProps {
|
|
message: InAppMessage | null;
|
|
onDismiss: () => void;
|
|
}
|
|
|
|
/**
|
|
* Placeholder modal component — product apps should implement their own
|
|
* styled version. Returns null.
|
|
*/
|
|
export function BroadcastModal(_props: BroadcastModalProps): React.JSX.Element | null {
|
|
return null;
|
|
}
|