Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-ways-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rozenite/controls-plugin': minor
---

Adds a new Controls plugin for exposing app-owned text values, toggles, and actions directly in React Native DevTools.
1 change: 1 addition & 0 deletions apps/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@rozenite/metro": "workspace:*",
"@rozenite/mmkv-plugin": "workspace:*",
"@rozenite/network-activity-plugin": "workspace:*",
"@rozenite/controls-plugin": "workspace:*",
"@rozenite/overlay-plugin": "workspace:*",
"@rozenite/performance-monitor-plugin": "workspace:*",
"@rozenite/react-navigation-plugin": "workspace:*",
Expand Down
10 changes: 10 additions & 0 deletions apps/playground/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
NavigationContainerRef,
} from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useRozeniteControlsPlugin } from '@rozenite/controls-plugin';
import { useMMKVDevTools } from '@rozenite/mmkv-plugin';
import { useNetworkActivityDevTools } from '@rozenite/network-activity-plugin';

Check warning on line 8 in apps/playground/src/app/App.tsx

View workflow job for this annotation

GitHub Actions / Validate

'/home/runner/work/rozenite/rozenite/apps/playground/node_modules/@rozenite/network-activity-plugin/dist/react-native.js' imported multiple times
import { usePerformanceMonitorDevTools } from '@rozenite/performance-monitor-plugin';
import { useReactNavigationDevTools } from '@rozenite/react-navigation-plugin';
import { useRozeniteStoragePlugin } from '@rozenite/storage-plugin';
Expand All @@ -13,11 +14,13 @@
import { useRef } from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider } from 'react-redux';
import { usePlaygroundControlsSections } from './hooks/usePlaygroundControlsSections';
import { mmkvStorages } from './mmkv-storages';
import { BottomTabNavigator } from './navigation/BottomTabNavigator';
import { SuccessiveScreensNavigator } from './navigation/SuccessiveScreensNavigator';
import { RootStackParamList } from './navigation/types';
import { ConfigScreen } from './screens/ConfigScreen';
import { ControlsPluginScreen } from './screens/ControlsPluginScreen';
import { LandingScreen } from './screens/LandingScreen';
import { MMKVPluginScreen } from './screens/MMKVPluginScreen';
import { NetworkTestScreen } from './screens/NetworkTestScreen';
Expand All @@ -30,7 +33,7 @@
import { storagePluginAdapters } from './storage-plugin-adapters';
import { primaryStore } from './store';
import { useRequireProfilerDevTools } from '@rozenite/require-profiler-plugin';
import { withOnBootNetworkActivityRecording } from '@rozenite/network-activity-plugin';

Check warning on line 36 in apps/playground/src/app/App.tsx

View workflow job for this annotation

GitHub Actions / Validate

'/home/runner/work/rozenite/rozenite/apps/playground/node_modules/@rozenite/network-activity-plugin/dist/react-native.js' imported multiple times
import { RozeniteOverlay } from '@rozenite/overlay-plugin';

withOnBootNetworkActivityRecording();
Expand All @@ -39,7 +42,12 @@
const Stack = createNativeStackNavigator<RootStackParamList>();

const Wrapper = () => {
const controlsSections = usePlaygroundControlsSections();

useTanStackQueryDevTools(queryClient);
useRozeniteControlsPlugin({
sections: controlsSections,
});
useNetworkActivityDevTools({
clientUISettings: {
showUrlAsName: true,
Expand All @@ -65,6 +73,7 @@
}}
>
<Stack.Screen name="Landing" component={LandingScreen} />
<Stack.Screen name="ControlsPlugin" component={ControlsPluginScreen} />
<Stack.Screen name="MMKVPlugin" component={MMKVPluginScreen} />
<Stack.Screen name="StoragePlugin" component={StoragePluginScreen} />
<Stack.Screen name="NetworkTest" component={NetworkTestScreen} />
Expand Down Expand Up @@ -110,6 +119,7 @@
config: {
screens: {
Landing: '',
ControlsPlugin: 'controls',
MMKVPlugin: 'mmkv',
StoragePlugin: 'storage',
NetworkTest: 'network',
Expand Down
257 changes: 257 additions & 0 deletions apps/playground/src/app/hooks/usePlaygroundControlsSections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { createSection } from '@rozenite/controls-plugin';
import { useMemo } from 'react';
import { useControlsPluginStore } from '../stores/controlsPluginStore';

export const usePlaygroundControlsSections = () => {
const counter = useControlsPluginStore((state) => state.counter);
const releaseLabel = useControlsPluginStore((state) => state.releaseLabel);
const selectedEnvironment = useControlsPluginStore(
(state) => state.selectedEnvironment
);
const status = useControlsPluginStore((state) => state.status);
const lastActionAt = useControlsPluginStore((state) => state.lastActionAt);
const notes = useControlsPluginStore((state) => state.notes);
const featureFlags = useControlsPluginStore((state) => state.featureFlags);
const updateReleaseLabel = useControlsPluginStore(
(state) => state.updateReleaseLabel
);
const selectEnvironment = useControlsPluginStore(
(state) => state.selectEnvironment
);
const toggleFlag = useControlsPluginStore((state) => state.toggleFlag);
const incrementCounter = useControlsPluginStore((state) => state.incrementCounter);
const markSynced = useControlsPluginStore((state) => state.markSynced);
const addCheckpoint = useControlsPluginStore((state) => state.addCheckpoint);
const resetDemo = useControlsPluginStore((state) => state.resetDemo);

return useMemo(
() => [
createSection({
id: 'controls-status',
title: status === 'synced' ? 'Sync Status' : 'Runtime Status',
description:
'This section reorders items when reverseDiagnostics changes, which makes HMR and full snapshot replacement easy to verify.',
items: featureFlags.reverseDiagnostics
? [
{
id: 'last-action',
type: 'text' as const,
title: 'Last Action',
value: lastActionAt ?? 'No actions yet',
},
{
id: 'counter',
type: 'text' as const,
title: 'Counter',
value: String(counter),
},
{
id: 'status',
type: 'text' as const,
title: 'Status',
value: status,
},
{
id: 'environment',
type: 'text' as const,
title: 'Environment',
value: selectedEnvironment,
},
{
id: 'release-label',
type: 'text' as const,
title: 'Release Label',
value: releaseLabel,
},
]
: [
{
id: 'status',
type: 'text' as const,
title: 'Status',
value: status,
},
{
id: 'counter',
type: 'text' as const,
title: 'Counter',
value: String(counter),
},
{
id: 'environment',
type: 'text' as const,
title: 'Environment',
value: selectedEnvironment,
},
{
id: 'release-label',
type: 'text' as const,
title: 'Release Label',
value: releaseLabel,
},
{
id: 'last-action',
type: 'text' as const,
title: 'Last Action',
value: lastActionAt ?? 'No actions yet',
},
],
}),
createSection({
id: 'feature-flags',
title: 'Feature Flags',
description: 'These toggles are handled on the device and mirrored into DevTools.',
items: [
{
id: 'verbose-logging',
type: 'toggle' as const,
title: 'Verbose Logging',
value: featureFlags.verboseLogging,
description: 'Changes status to armed when enabled.',
onUpdate: (nextValue: boolean) => toggleFlag('verboseLogging', nextValue),
},
{
id: 'mock-latency',
type: 'toggle' as const,
title: 'Mock Latency',
value: featureFlags.mockLatency,
description: 'Pure state toggle for validating round-trips.',
validate: (nextValue: boolean) =>
status === 'synced' && nextValue
? {
valid: false,
message:
'Disable synced status before enabling mock latency.',
}
: { valid: true },
onUpdate: (nextValue: boolean) => toggleFlag('mockLatency', nextValue),
},
{
id: 'reverse-diagnostics',
type: 'toggle' as const,
title: 'Reverse Diagnostics',
value: featureFlags.reverseDiagnostics,
description: 'Reorders the diagnostics section to prove full snapshot replacement.',
onUpdate: (nextValue: boolean) => toggleFlag('reverseDiagnostics', nextValue),
},
{
id: 'blocked-toggle',
type: 'toggle' as const,
title: 'Blocked Toggle',
value: false,
description:
'This toggle is intentionally rejected to make validation errors easy to test.',
validate: (nextValue: boolean) =>
nextValue
? {
valid: false,
message: 'This demo toggle cannot be enabled.',
}
: { valid: true },
onUpdate: () => undefined,
},
{
id: 'environment-selector',
type: 'select' as const,
title: 'Environment',
value: selectedEnvironment,
description: 'Choose the backend target directly from DevTools.',
options: [
{ label: 'Local', value: 'local' },
{ label: 'Staging', value: 'staging' },
{ label: 'Production', value: 'production' },
],
validate: (nextValue: string) =>
counter > 0 && nextValue === 'production'
? {
valid: false,
message:
'Reset the counter before switching to production.',
}
: { valid: true },
onUpdate: (nextValue: string) =>
selectEnvironment(
nextValue as 'local' | 'staging' | 'production'
),
},
{
id: 'release-label-input',
type: 'input' as const,
title: 'Release Label',
value: releaseLabel,
placeholder: 'build-001',
applyLabel: 'Apply',
description:
'Apply is enabled only after editing. Labels must be at least 3 characters and contain no spaces.',
validate: (nextValue: string) =>
nextValue.trim().length < 3
? {
valid: false,
message: 'Release label must be at least 3 characters.',
}
: nextValue.includes(' ')
? {
valid: false,
message: 'Release label cannot contain spaces.',
}
: { valid: true },
onUpdate: (nextValue: string) => updateReleaseLabel(nextValue),
},
],
}),
createSection({
id: 'actions',
title: 'Actions',
description: `Recent checkpoints tracked: ${notes.length}`,
items: [
{
id: 'increment-counter',
type: 'button' as const,
title: 'Increment Counter',
actionLabel: 'Increment',
onPress: incrementCounter,
},
{
id: 'mark-synced',
type: 'button' as const,
title: 'Mark Synced',
actionLabel: 'Sync',
onPress: markSynced,
},
{
id: 'add-checkpoint',
type: 'button' as const,
title: 'Add Checkpoint',
actionLabel: 'Add',
onPress: addCheckpoint,
},
{
id: 'reset-demo',
type: 'button' as const,
title: 'Reset Demo',
actionLabel: 'Reset',
onPress: resetDemo,
},
],
}),
],
[
addCheckpoint,
counter,
featureFlags.mockLatency,
featureFlags.reverseDiagnostics,
featureFlags.verboseLogging,
incrementCounter,
lastActionAt,
markSynced,
notes.length,
releaseLabel,
resetDemo,
selectEnvironment,
selectedEnvironment,
status,
toggleFlag,
updateReleaseLabel,
]
);
};
1 change: 1 addition & 0 deletions apps/playground/src/app/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';

export type RootStackParamList = {
Landing: undefined;
ControlsPlugin: undefined;
MMKVPlugin: undefined;
StoragePlugin: undefined;
NetworkTest: undefined;
Expand Down
Loading
Loading