From 9cbeec64cd15721c2564554e227a6c9a33fa429f Mon Sep 17 00:00:00 2001 From: Teodor Ciuraru Date: Mon, 18 Aug 2025 16:53:55 +0300 Subject: [PATCH 1/8] feat(react-native): add Windows support for React Native quickstart app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add full Windows project structure via React Native Windows - Implement Ditto singleton pattern to handle Windows component lifecycle - Replace native Modal with custom overlay for proper Windows rendering - Add NuGet.config for Windows package resolution - Update README with Windows prerequisites and setup instructions - Revert Ditto SDK from local path to stable version 4.12.0 - Remove package-lock.json in favor of yarn.lock Windows-specific improvements: - Prevents multiple Ditto instances during component remounting - Modal renders correctly within window bounds - Handles Fast Refresh gracefully with singleton pattern 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- react-native/.claude/settings.local.json | 11 + react-native/.gitignore | 3 + react-native/App.tsx | 111 +- react-native/NuGet.config | 11 + react-native/README.md | 25 +- react-native/components/NewTaskModal.tsx | 130 +- react-native/dittoSingleton.ts | 31 + react-native/metro.config.js | 40 +- react-native/package-lock.json | 15333 ---------------- react-native/package.json | 22 +- react-native/windows/.gitignore | 46 + .../DittoReactNativeSampleApp.Package.wapproj | 78 + .../Images/LockScreenLogo.scale-200.png | Bin 0 -> 1430 bytes .../Images/SplashScreen.scale-200.png | Bin 0 -> 7700 bytes .../Images/Square150x150Logo.scale-200.png | Bin 0 -> 2937 bytes .../Images/Square44x44Logo.scale-200.png | Bin 0 -> 1647 bytes ...x44Logo.targetsize-24_altform-unplated.png | Bin 0 -> 1255 bytes .../Images/StoreLogo.png | Bin 0 -> 1451 bytes .../Images/Wide310x150Logo.scale-200.png | Bin 0 -> 3204 bytes .../Package.appxmanifest | 49 + .../packages.lock.json | 272 + .../windows/DittoReactNativeSampleApp.sln | 77 + .../DittoReactNativeSampleApp/.gitignore | 1 + .../AutolinkedNativeModules.g.cpp | 18 + .../AutolinkedNativeModules.g.h | 10 + .../AutolinkedNativeModules.g.props | 6 + .../AutolinkedNativeModules.g.targets | 10 + .../DittoReactNativeSampleApp.cpp | 82 + .../DittoReactNativeSampleApp.h | 3 + .../DittoReactNativeSampleApp.ico | Bin 0 -> 46227 bytes .../DittoReactNativeSampleApp.rc | Bin 0 -> 3316 bytes .../DittoReactNativeSampleApp.vcxproj | 135 + .../DittoReactNativeSampleApp.vcxproj.filters | 58 + .../packages.lock.json | 198 + .../windows/DittoReactNativeSampleApp/pch.cpp | 1 + .../windows/DittoReactNativeSampleApp/pch.h | 38 + .../DittoReactNativeSampleApp/resource.h | 17 + .../DittoReactNativeSampleApp/small.ico | Bin 0 -> 46227 bytes .../DittoReactNativeSampleApp/targetver.h | 8 + .../windows/ExperimentalFeatures.props | 27 + react-native/yarn.lock | 4392 +++-- 41 files changed, 3505 insertions(+), 17738 deletions(-) create mode 100644 react-native/.claude/settings.local.json create mode 100644 react-native/NuGet.config create mode 100644 react-native/dittoSingleton.ts delete mode 100644 react-native/package-lock.json create mode 100644 react-native/windows/.gitignore create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/DittoReactNativeSampleApp.Package.wapproj create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/Images/LockScreenLogo.scale-200.png create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/Images/SplashScreen.scale-200.png create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/Images/Square150x150Logo.scale-200.png create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/Images/Square44x44Logo.scale-200.png create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/Images/Square44x44Logo.targetsize-24_altform-unplated.png create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/Images/StoreLogo.png create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/Images/Wide310x150Logo.scale-200.png create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/Package.appxmanifest create mode 100644 react-native/windows/DittoReactNativeSampleApp.Package/packages.lock.json create mode 100644 react-native/windows/DittoReactNativeSampleApp.sln create mode 100644 react-native/windows/DittoReactNativeSampleApp/.gitignore create mode 100644 react-native/windows/DittoReactNativeSampleApp/AutolinkedNativeModules.g.cpp create mode 100644 react-native/windows/DittoReactNativeSampleApp/AutolinkedNativeModules.g.h create mode 100644 react-native/windows/DittoReactNativeSampleApp/AutolinkedNativeModules.g.props create mode 100644 react-native/windows/DittoReactNativeSampleApp/AutolinkedNativeModules.g.targets create mode 100644 react-native/windows/DittoReactNativeSampleApp/DittoReactNativeSampleApp.cpp create mode 100644 react-native/windows/DittoReactNativeSampleApp/DittoReactNativeSampleApp.h create mode 100644 react-native/windows/DittoReactNativeSampleApp/DittoReactNativeSampleApp.ico create mode 100644 react-native/windows/DittoReactNativeSampleApp/DittoReactNativeSampleApp.rc create mode 100644 react-native/windows/DittoReactNativeSampleApp/DittoReactNativeSampleApp.vcxproj create mode 100644 react-native/windows/DittoReactNativeSampleApp/DittoReactNativeSampleApp.vcxproj.filters create mode 100644 react-native/windows/DittoReactNativeSampleApp/packages.lock.json create mode 100644 react-native/windows/DittoReactNativeSampleApp/pch.cpp create mode 100644 react-native/windows/DittoReactNativeSampleApp/pch.h create mode 100644 react-native/windows/DittoReactNativeSampleApp/resource.h create mode 100644 react-native/windows/DittoReactNativeSampleApp/small.ico create mode 100644 react-native/windows/DittoReactNativeSampleApp/targetver.h create mode 100644 react-native/windows/ExperimentalFeatures.props diff --git a/react-native/.claude/settings.local.json b/react-native/.claude/settings.local.json new file mode 100644 index 000000000..2fa9c9274 --- /dev/null +++ b/react-native/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(yarn install)", + "Bash(rm -f package-lock.json)", + "Bash(git add:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/react-native/.gitignore b/react-native/.gitignore index d5ae45669..e3e531eab 100644 --- a/react-native/.gitignore +++ b/react-native/.gitignore @@ -72,3 +72,6 @@ yarn-error.log !.yarn/releases !.yarn/sdks !.yarn/versions + +# Ditto +ditto \ No newline at end of file diff --git a/react-native/App.tsx b/react-native/App.tsx index 1b318e90c..3def3f8ba 100644 --- a/react-native/App.tsx +++ b/react-native/App.tsx @@ -22,6 +22,7 @@ import { DITTO_AUTH_URL, DITTO_WEBSOCKET_URL, } from '@env'; +import { getDittoInstance, setDittoInstance } from './dittoSingleton'; import Fab from './components/Fab'; import NewTaskModal from './components/NewTaskModal'; @@ -139,9 +140,36 @@ const App = () => { }; const initDitto = async () => { + // Check for existing global instance first + const existingInstance = getDittoInstance(); + if (existingInstance) { + ditto.current = existingInstance; + + // Re-register observers for this component + taskObserver.current = ditto.current.store.registerObserver( + 'SELECT * FROM tasks WHERE NOT deleted', + response => { + const fetchedTasks: Task[] = response.items.map(doc => ({ + id: doc.value._id, + title: doc.value.title as string, + done: doc.value.done, + deleted: doc.value.deleted, + })); + setTasks(fetchedTasks); + }, + ); + return; + } + + // Prevent multiple Ditto instances + if (ditto.current) { + return; + } + try { // https://docs.ditto.live/sdk/latest/install-guides/react-native#onlineplayground ditto.current = new Ditto(identity); + setDittoInstance(ditto.current); // Initialize transport config ditto.current.updateTransportConfig(config => { @@ -185,18 +213,32 @@ const App = () => { }; useEffect(() => { + let mounted = true; + (async () => { const granted = Platform.OS === 'android' ? await requestPermissions() : true; - if (granted) { + if (granted && mounted) { initDitto(); - } else { + } else if (!granted) { Alert.alert( 'Permission Denied', 'You need to grant all permissions to use this app.', ); } })(); + + // Cleanup function + return () => { + mounted = false; + if (ditto.current) { + console.log('Cleaning up Ditto instance'); + ditto.current.stopSync(); + taskObserver.current?.cancel(); + taskSubscription.current?.cancel(); + // Note: We don't set ditto.current to null here to prevent re-initialization + } + }; }, []); const renderItem = ({item}: {item: Task}) => ( @@ -217,44 +259,49 @@ const App = () => { return ( - - - setModalVisible(true)} /> - setModalVisible(false)} - onSubmit={task => { - createTask(task); - setModalVisible(false); - }} - onClose={() => setModalVisible(false)} - /> - setEditingTask(null)} - onSubmit={(taskId, newTitle) => { - updateTaskTitle(taskId, newTitle); - setEditingTask(null); - }} - onClose={() => setEditingTask(null)} - /> - item.id} - /> + + + + setModalVisible(true)} /> + item.id} + /> + { + createTask(task); + setModalVisible(false); + }} + onClose={() => setModalVisible(false)} + /> + setEditingTask(null)} + onSubmit={(taskId, newTitle) => { + updateTaskTitle(taskId, newTitle); + setEditingTask(null); + }} + onClose={() => setEditingTask(null)} + /> + ); }; const styles = StyleSheet.create({ container: { - height: '100%', - padding: 20, + flex: 1, backgroundColor: '#fff', }, + appContainer: { + flex: 1, + padding: 20, + position: 'relative', + }, listContainer: { gap: 5, }, diff --git a/react-native/NuGet.config b/react-native/NuGet.config new file mode 100644 index 000000000..fe459fedd --- /dev/null +++ b/react-native/NuGet.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/react-native/README.md b/react-native/README.md index bdbacc797..c7aafe2b1 100644 --- a/react-native/README.md +++ b/react-native/README.md @@ -18,6 +18,15 @@ A sample React Native application that lets you create tasks and sync them with - **Ditto Portal Account**: Ensure you have a Ditto account. Sign up [here](https://portal.ditto.live/signup). - **App Credentials**: After registration, create an application within the Ditto Portal to obtain your `AppID`, `Online Playground Token`, `Auth URL`, and `Websocket URL`. Visit the [Ditto Portal](https://portal.ditto.live/) to manage your applications. +### Windows-specific Prerequisites + +- **Windows 10 or 11** (version 10.0.19041.0 or higher) +- **Visual Studio 2022** with the following workloads: + - Universal Windows Platform development + - Desktop development with C++ + - Node.js development (under Individual Components) +- **Developer Mode**: Enabled in Windows Settings > Update & Security > For developers + ## Getting Started ### Install Dependencies @@ -49,6 +58,12 @@ For Android: yarn react-native run-android ``` +For Windows: + +```bash +yarn windows +``` + ## Features - **Task Creation**: Users can add new tasks to their list. @@ -56,7 +71,15 @@ yarn react-native run-android ## Additional Information -- Limitation: React Native's Fast Refresh must be disabled and it's something we're working on fixing. +### Windows-specific Notes + +- **NuGet.config**: Required for Windows to resolve React Native Windows packages from the correct sources. +- **Ditto Singleton Pattern**: The app uses a singleton pattern (`dittoSingleton.ts`) to prevent multiple Ditto instances, particularly important for Windows which may remount components more frequently than other platforms. +- **Custom Modal Implementation**: Windows uses a custom modal overlay instead of the native Modal component to ensure proper rendering within window bounds. + +### Known Limitations + +- React Native's Fast Refresh may cause file lock issues with Ditto on Windows due to component remounting. The singleton pattern mitigates this issue. ## iOS Installation diff --git a/react-native/components/NewTaskModal.tsx b/react-native/components/NewTaskModal.tsx index e8176965a..7209c4451 100644 --- a/react-native/components/NewTaskModal.tsx +++ b/react-native/components/NewTaskModal.tsx @@ -1,68 +1,126 @@ import React from 'react'; import {useState} from 'react'; import { - Button, - Modal, - ModalProps, StyleSheet, Text, TextInput, View, + Platform, + TouchableOpacity, } from 'react-native'; type NewTaskModalProps = { + visible?: boolean; onSubmit: (taskName: string) => void; onClose?: () => void; }; -type Props = NewTaskModalProps & ModalProps; - -const NewTaskModal: React.FC = ({onSubmit, onClose, ...props}) => { +const NewTaskModal: React.FC = ({visible, onSubmit, onClose}) => { const [input, setInput] = useState(''); const submit = () => { - if (input !== '') { - onSubmit(input); + if (input.trim() !== '') { + onSubmit(input.trim()); setInput(''); + onClose?.(); } }; - return ( - - - + if (!visible) return null; + + // For Windows, render as an absolute positioned overlay within the app + if (Platform.OS === 'windows') { + return ( + + New Task -