import { useEffect, useState } from 'react'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import { AppState, Modal, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'; import { useAuthStore, type AuthState } from '../store/auth-store'; import { useInboxStore } from '../store/inbox-store'; import { useNotesStore } from '../store/notes-store'; import { useWorkspaceStore } from '../store/workspace-store'; import { checkKillSwitch, flushTelemetry, initPlatform } from '../lib/platform'; import { getBroadcastClient } from '../lib/broadcast-client'; import { getSurveyClient } from '../lib/survey-client'; import { flushNoteQueue, getNoteQueueSize } from '../lib/offline-queue'; import type { InAppMessage } from '@bytelyst/broadcast-client'; import type { ActiveSurvey, Question, QuestionAnswer } from '@bytelyst/survey-client'; import { colors } from '../theme'; export default function RootLayout() { const [broadcastMessages, setBroadcastMessages] = useState([]); const [activeSurvey, setActiveSurvey] = useState(null); const [surveyStarted, setSurveyStarted] = useState(false); const [surveyIndex, setSurveyIndex] = useState(0); const [surveyAnswers, setSurveyAnswers] = useState>({}); const [textAnswer, setTextAnswer] = useState(''); const [queueStatus, setQueueStatus] = useState<{ pending: number; lastFlushed: number }>({ pending: getNoteQueueSize(), lastFlushed: 0, }); const [killSwitchState, setKillSwitchState] = useState<{ checked: boolean; disabled: boolean; message: string | null; }>({ checked: false, disabled: false, message: null, }); const bootstrapAuth = useAuthStore((state: AuthState) => state.bootstrap); const hasBootstrapped = useAuthStore((state: AuthState) => state.hasBootstrapped); const isAuthenticated = useAuthStore((state: AuthState) => state.isAuthenticated); function toSurveyAnswer(question: Question, value: string): QuestionAnswer { if (question.type === 'single_choice' || question.type === 'dropdown') { return { type: 'single_choice', optionId: value }; } if (question.type === 'multiple_choice') { return { type: 'multiple_choice', optionIds: [value] }; } if (question.type === 'ranking') { return { type: 'ranking', rankedOptionIds: [value] }; } if (question.type === 'rating' || question.type === 'nps' || question.type === 'scale') { return { type: 'rating', value: Number(value) }; } return { type: 'text', value }; } async function flushQueuedNoteMutations(): Promise { const pending = getNoteQueueSize(); if (pending === 0) { setQueueStatus({ pending: 0, lastFlushed: 0 }); return; } const result = await flushNoteQueue().catch(() => ({ flushed: 0, failed: getNoteQueueSize() })); setQueueStatus({ pending: result.failed, lastFlushed: result.flushed, }); } async function loadBroadcasts(): Promise { try { const { messages } = await getBroadcastClient().listMessages(); setBroadcastMessages(messages.filter((message) => message.status !== 'dismissed')); } catch { } } async function loadSurvey(): Promise { try { const { survey } = await getSurveyClient().getActiveSurvey(); setActiveSurvey((current) => current ?? survey); } catch { } } useEffect(() => { void checkKillSwitch() .then((state) => { setKillSwitchState({ checked: true, disabled: state.disabled, message: state.message, }); }) .catch(() => { setKillSwitchState({ checked: true, disabled: false, message: null }); }); }, []); useEffect(() => { void bootstrapAuth(); }, [bootstrapAuth]); useEffect(() => { void initPlatform(); }, []); useEffect(() => { const sub = AppState.addEventListener('change', (nextState) => { if (nextState === 'background' || nextState === 'inactive') { flushTelemetry(); } }); return () => sub.remove(); }, []); useEffect(() => { if (!hasBootstrapped || !isAuthenticated) { return undefined; } let cancelled = false; void (async () => { await Promise.all([ useNotesStore.getState().hydrate(), useWorkspaceStore.getState().hydrate(), useInboxStore.getState().hydrate(), ]); if (cancelled) { return; } void flushQueuedNoteMutations(); void loadBroadcasts(); void loadSurvey(); })(); const broadcastTimer = setInterval(() => { void loadBroadcasts(); }, 5 * 60_000); const surveyTimer = setInterval(() => { void loadSurvey(); }, 10 * 60_000); const appStateSubscription = AppState.addEventListener('change', (nextState) => { if (nextState === 'active') { void flushQueuedNoteMutations(); } }); return () => { cancelled = true; clearInterval(broadcastTimer); clearInterval(surveyTimer); appStateSubscription.remove(); }; }, [hasBootstrapped, isAuthenticated]); if (killSwitchState.checked && killSwitchState.disabled) { return ( NoteLett is temporarily unavailable {killSwitchState.message ?? 'Service access is currently paused by the platform team. Please try again later.'} ); } const currentQuestion = activeSurvey?.questions[surveyIndex]; async function dismissBroadcast(messageId: string): Promise { try { await getBroadcastClient().markDismissed(messageId); } catch { } setBroadcastMessages((current) => current.filter((message) => message.id !== messageId)); } async function startSurvey(): Promise { if (!activeSurvey) { return; } try { await getSurveyClient().startSurvey(activeSurvey.id); } catch { } setSurveyStarted(true); } async function dismissSurvey(): Promise { if (activeSurvey) { try { await getSurveyClient().dismissSurvey(activeSurvey.id); } catch { } } setActiveSurvey(null); setSurveyStarted(false); setSurveyIndex(0); setSurveyAnswers({}); setTextAnswer(''); } async function submitAnswer(value: string): Promise { if (!activeSurvey || !currentQuestion) { return; } const answer = toSurveyAnswer(currentQuestion, value); setSurveyAnswers((current) => ({ ...current, [currentQuestion.id]: answer, })); try { await getSurveyClient().submitAnswer(activeSurvey.id, currentQuestion.id, answer); } catch { return; } const isLastQuestion = surveyIndex >= activeSurvey.questions.length - 1; if (isLastQuestion) { try { await getSurveyClient().completeSurvey(activeSurvey.id); } catch { } await dismissSurvey(); return; } setSurveyIndex((current) => current + 1); setTextAnswer(''); } return ( {broadcastMessages.length > 0 ? ( {broadcastMessages.map((message) => ( {message.title} {message.body ? {message.body} : null} {message.dismissible !== false ? ( void dismissBroadcast(message.id)}> × ) : null} ))} ) : null} {queueStatus.pending > 0 || queueStatus.lastFlushed > 0 ? ( Offline sync Pending actions: {queueStatus.pending} Flushed on last attempt: {queueStatus.lastFlushed} ) : null} {activeSurvey && !surveyStarted ? ( {activeSurvey.title} void startSurvey()}> Start void dismissSurvey()}> Dismiss ) : null} {activeSurvey?.title} {currentQuestion?.text} {surveyIndex + 1}/{activeSurvey?.questions.length ?? 1} {currentQuestion?.options?.length ? ( {currentQuestion.options.map((option) => ( void submitAnswer(option.id)} > {option.emoji ? `${option.emoji} ` : ''}{option.text} ))} ) : currentQuestion && (currentQuestion.type === 'rating' || currentQuestion.type === 'nps' || currentQuestion.type === 'scale') ? ( {Array.from( { length: (currentQuestion.maxValue ?? 5) - (currentQuestion.minValue ?? 1) + 1 }, (_, index) => (currentQuestion.minValue ?? 1) + index, ).map((value) => ( void submitAnswer(String(value))}> {value} ))} ) : ( <> void submitAnswer(textAnswer.trim())} > Next )} void dismissSurvey()}> Close ); } const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: colors.bgCanvas, }, queueStatusCard: { marginHorizontal: 12, borderRadius: 12, borderWidth: 1, borderColor: colors.borderDefault, backgroundColor: colors.surfaceCard, padding: 10, gap: 4, }, queueStatusTitle: { color: colors.textPrimary, fontWeight: '700', fontSize: 13, }, queueStatusBody: { color: colors.textSecondary, fontSize: 12, }, bannerSection: { paddingHorizontal: 12, paddingTop: 10, gap: 8, }, bannerCard: { borderRadius: 12, borderWidth: 1, borderColor: colors.borderDefault, backgroundColor: colors.bgElevated, padding: 10, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', }, bannerContent: { flex: 1, gap: 4, paddingRight: 10, }, bannerTitle: { color: colors.textPrimary, fontWeight: '700', fontSize: 13, }, bannerBody: { color: colors.textSecondary, fontSize: 12, }, bannerDismiss: { color: colors.textSecondary, fontSize: 18, lineHeight: 18, }, surveyPrompt: { margin: 12, borderRadius: 12, borderWidth: 1, borderColor: colors.borderDefault, backgroundColor: colors.surfaceCard, padding: 12, gap: 10, }, surveyPromptText: { color: colors.textPrimary, fontWeight: '700', }, surveyActions: { flexDirection: 'row', gap: 8, }, primaryButton: { borderRadius: 10, backgroundColor: colors.accentPrimary, paddingHorizontal: 14, paddingVertical: 10, alignItems: 'center', justifyContent: 'center', }, primaryButtonText: { color: colors.textPrimary, fontWeight: '700', }, secondaryButton: { borderRadius: 10, borderWidth: 1, borderColor: colors.borderDefault, paddingHorizontal: 14, paddingVertical: 10, alignItems: 'center', justifyContent: 'center', }, secondaryButtonText: { color: colors.textSecondary, fontWeight: '600', }, stackContainer: { flex: 1, }, modalBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.45)', justifyContent: 'center', paddingHorizontal: 16, }, modalCard: { borderRadius: 16, borderWidth: 1, borderColor: colors.borderDefault, backgroundColor: colors.bgCanvas, padding: 16, gap: 12, maxHeight: '85%', }, modalTitle: { color: colors.textPrimary, fontWeight: '700', fontSize: 18, }, modalQuestion: { color: colors.textPrimary, fontSize: 15, lineHeight: 21, }, modalMeta: { color: colors.textSecondary, fontSize: 12, }, optionsList: { gap: 8, }, optionButton: { borderRadius: 10, borderWidth: 1, borderColor: colors.borderDefault, paddingHorizontal: 12, paddingVertical: 10, }, optionText: { color: colors.textPrimary, fontSize: 14, }, ratingRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, }, ratingButton: { width: 38, height: 38, borderRadius: 19, borderWidth: 1, borderColor: colors.borderDefault, alignItems: 'center', justifyContent: 'center', }, ratingText: { color: colors.textPrimary, fontWeight: '600', }, answerInput: { borderRadius: 10, borderWidth: 1, borderColor: colors.borderDefault, backgroundColor: colors.bgElevated, color: colors.textPrimary, padding: 10, minHeight: 42, textAlignVertical: 'top' as const, }, disabledButton: { opacity: 0.5, }, });