- 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
126 lines
3.4 KiB
TypeScript
126 lines
3.4 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('/broadcasts');
|
|
if (!res.ok) return;
|
|
const data = (await res.json()) as {
|
|
messages?: Array<{
|
|
id: string;
|
|
title: string;
|
|
body: string;
|
|
ctaText?: string;
|
|
ctaUrl?: string;
|
|
priority?: 'low' | 'normal' | 'high' | 'urgent';
|
|
dismissible?: boolean;
|
|
expiresAt?: string;
|
|
}>;
|
|
};
|
|
const raw = data.messages ?? [];
|
|
const mapped: InAppMessage[] = raw.map(m => ({
|
|
id: m.id,
|
|
title: m.title,
|
|
body: m.body,
|
|
type:
|
|
m.priority === 'urgent' ? 'critical' : m.priority === 'high' ? 'warning' : 'info',
|
|
action:
|
|
m.ctaText && m.ctaUrl ? { label: m.ctaText, url: m.ctaUrl } : undefined,
|
|
dismissible: m.dismissible !== false,
|
|
expiresAt: m.expiresAt,
|
|
}));
|
|
setMessages(mapped);
|
|
} catch {
|
|
/* silent */
|
|
}
|
|
}, [sdk]);
|
|
|
|
const dismiss = useCallback(
|
|
(id: string) => {
|
|
setMessages(prev => prev.filter(m => m.id !== id));
|
|
sdk.fetch(`/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;
|
|
}
|