diff --git a/examples/demo/src/components/LogView.tsx b/examples/demo/src/components/LogView.tsx new file mode 100644 index 00000000..4b213150 --- /dev/null +++ b/examples/demo/src/components/LogView.tsx @@ -0,0 +1,109 @@ +import React, { useRef, useState } from 'react'; +import { Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; + +import { clearLogs, useLogs } from '../services/LogService'; +import { AppColors } from '../theme'; + +const MONOSPACE = Platform.select({ ios: 'Menlo', default: 'monospace' }); + +export default function LogView() { + const logs = useLogs(); + const scrollRef = useRef(null); + const [collapsed, setCollapsed] = useState(false); + const scrollToEnd = () => scrollRef.current?.scrollToEnd({ animated: false }); + + return ( + + + LOGS ({logs.length}) + + + + + setCollapsed((value) => !value)} + hitSlop={8} + accessibilityRole="button" + accessibilityLabel={collapsed ? 'Expand logs' : 'Collapse logs'} + testID="log_toggle_button" + > + + + + + {!collapsed && ( + + {logs.length === 0 ? ( + No logs yet + ) : ( + logs.map((entry, index) => ( + + {entry.time} {entry.message} + + )) + )} + + )} + + ); +} + +const ENTRY_FONT_SIZE = 8; + +const styles = StyleSheet.create({ + container: { + backgroundColor: AppColors.osLogBackground, + }, + header: { + height: 24, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + }, + headerText: { + color: AppColors.osGrey500, + fontFamily: MONOSPACE, + fontSize: 11, + fontWeight: '700', + letterSpacing: 0.5, + }, + actions: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + body: { + height: 100, + }, + bodyContent: { + paddingHorizontal: 16, + paddingBottom: 6, + }, + entry: { + color: AppColors.osLogText, + fontFamily: MONOSPACE, + fontSize: ENTRY_FONT_SIZE, + lineHeight: ENTRY_FONT_SIZE * 1.4, + }, + muted: { + color: AppColors.osGrey600, + }, +}); diff --git a/examples/demo/src/hooks/useOneSignal.ts b/examples/demo/src/hooks/useOneSignal.ts index 06177ef6..551c549f 100644 --- a/examples/demo/src/hooks/useOneSignal.ts +++ b/examples/demo/src/hooks/useOneSignal.ts @@ -24,6 +24,7 @@ import { } from 'react-native-onesignal'; import { NotificationType } from '../models/NotificationType'; +import { log } from '../services/LogService'; import OneSignalApiService from '../services/OneSignalApiService'; import PreferencesService from '../services/PreferencesService'; @@ -170,34 +171,37 @@ function useOneSignalState(): UseOneSignalReturn { let userChanged = false; const handleIamWillDisplay = (e: InAppMessageWillDisplayEvent) => { - console.log(`IAM willDisplay: ${e.message.messageId}`); + log(`IAM willDisplay: ${e.message.messageId}`); }; const handleIamDidDisplay = (e: InAppMessageDidDisplayEvent) => { - console.log(`IAM didDisplay: ${e.message.messageId}`); + log(`IAM didDisplay: ${e.message.messageId}`); }; const handleIamWillDismiss = (e: InAppMessageWillDismissEvent) => { - console.log(`IAM willDismiss: ${e.message.messageId}`); + log(`IAM willDismiss: ${e.message.messageId}`); }; const handleIamDidDismiss = (e: InAppMessageDidDismissEvent) => { - console.log(`IAM didDismiss: ${e.message.messageId}`); + log(`IAM didDismiss: ${e.message.messageId}`); }; const handleIamClick = (e: InAppMessageClickEvent) => { - console.log(`IAM click: ${e.message.messageId}`); + log(`IAM click: ${e.message.messageId}`); }; const handleNotificationClick = (e: NotificationClickEvent) => { - console.log(`Notification click: ${e.notification.title ?? ''}`); + log(`Notification click: ${e.notification.notificationId} ${e.notification.title ?? ''}`); // uncomment to see the full event object // console.log('[OneSignal] click event: ', e); }; const handleForegroundWillDisplay = (e: NotificationWillDisplayEvent) => { - console.log(`Notification foregroundWillDisplay: ${e.getNotification().title ?? ''}`); + const notification = e.getNotification(); + log( + `Notification foregroundWillDisplay: ${notification.notificationId} ${notification.title ?? ''}`, + ); // uncomment to see the full event object // console.log('[OneSignal] will display event: ', e.getNotification()); @@ -224,12 +228,18 @@ function useOneSignalState(): UseOneSignalReturn { const pushSubHandler = (event: PushSubscriptionChangedState) => { pushChanged = true; - setPushSubscriptionId(event.current.id ?? undefined); - setIsPushEnabled(event.current.optedIn); + const { previous, current } = event; + const fmtToken = (t: string | undefined) => (t ? `${t.slice(0, 8)}…` : 'null'); + log( + `Push subscription changed: id=${previous.id ?? 'null'} → ${current.id ?? 'null'}, optedIn=${previous.optedIn} → ${current.optedIn}, token=${fmtToken(previous.token)} → ${fmtToken(current.token)}`, + ); + setPushSubscriptionId(current.id ?? undefined); + setIsPushEnabled(current.optedIn); }; const permissionHandler = (granted: boolean) => { permissionChanged = true; + log(`Permission changed: ${granted}`); setHasNotificationPermission(granted); }; @@ -237,7 +247,7 @@ function useOneSignalState(): UseOneSignalReturn { userChanged = true; requestSequenceRef.current++; const nextOnesignalId = event.current.onesignalId ?? null; - console.log( + log( `User changed: onesignalId=${nextOnesignalId ?? 'null'}, externalId=${event.current.externalId ?? 'null'}`, ); diff --git a/examples/demo/src/screens/HomeScreen.tsx b/examples/demo/src/screens/HomeScreen.tsx index 61b95856..d6b7f9e3 100644 --- a/examples/demo/src/screens/HomeScreen.tsx +++ b/examples/demo/src/screens/HomeScreen.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react'; import { Platform, ScrollView, StyleSheet, View } from 'react-native'; import ActionButton from '../components/ActionButton'; +import LogView from '../components/LogView'; import TooltipModal from '../components/modals/TooltipModal'; import AliasesSection from '../components/sections/AliasesSection'; import AppSection from '../components/sections/AppSection'; @@ -47,6 +48,8 @@ export default function HomeScreen() { return ( + + void>(); + +function emit(): void { + for (const listener of listeners) listener(); +} + +function formatTime(date: Date): string { + return [date.getHours(), date.getMinutes(), date.getSeconds()] + .map((part) => String(part).padStart(2, '0')) + .join(':'); +} + +/** Write to the on-screen log view and the console. Kept in memory only, so each launch starts empty. */ +export function log(message: string): void { + console.log(`[OneSignal] ${message}`); + entries = [...entries, { id: nextId++, time: formatTime(new Date()), message }].slice( + -MAX_ENTRIES, + ); + emit(); +} + +export function clearLogs(): void { + entries = []; + emit(); +} + +function subscribe(listener: () => void): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +function getSnapshot(): LogEntry[] { + return entries; +} + +export function useLogs(): LogEntry[] { + return useSyncExternalStore(subscribe, getSnapshot); +} diff --git a/examples/demo/src/theme.ts b/examples/demo/src/theme.ts index 163589f2..78a414be 100644 --- a/examples/demo/src/theme.ts +++ b/examples/demo/src/theme.ts @@ -12,6 +12,8 @@ export const AppColors = { osDivider: '#E8EAED', osWarningBackground: '#FFF8E1', osBackdrop: 'rgba(0,0,0,0.54)', + osLogBackground: '#1E1E1E', + osLogText: '#D4D4D4', white: '#FFFFFF', } as const;