UNPKG

@dittolive/ditto

Version:

Ditto is a cross-platform SDK that allows apps to sync with and even without internet connectivity.

226 lines (201 loc) 7.72 kB
// Dynamic imports to avoid bundling Expo dependencies when not needed function loadExpoDependencies() { try { const { withInfoPlist, withAndroidManifest, withDangerousMod, createRunOncePlugin, ConfigPlugin, } = require('@expo/config-plugins') as any; const { withBuildProperties } = require('expo-build-properties') as any; return { withInfoPlist, withAndroidManifest, withDangerousMod, createRunOncePlugin, ConfigPlugin, withBuildProperties, }; } catch { throw new Error(` Ditto Expo plugin requires additional dependencies: npm install @expo/config-plugins expo-build-properties These are only needed when using Expo with Ditto. `); } } const BLE_USAGE = 'Uses Bluetooth to connect and sync with nearby devices.'; const LAN_USAGE = 'Uses WiFi to connect and sync with nearby devices.'; type DittoConfig = { /** * The iOS Bluetooth prompt's description. Defaults to "Uses Bluetooth to * connect and sync with nearby devices." */ bluetoothUsageDescription?: string; /** * The iOS LAN prompt's description. Defaults to "Uses WiFi to connect and * sync with nearby devices." */ localNetworkUsageDescription?: string; }; const withDittoIOS: any = (expoConfig: any, props: DittoConfig) => { const { withInfoPlist } = loadExpoDependencies(); return withInfoPlist(expoConfig, (config: any) => { const infoPlist = config.modResults; infoPlist.NSBluetoothAlwaysUsageDescription = props.bluetoothUsageDescription ?? infoPlist.NSBluetoothAlwaysUsageDescription ?? BLE_USAGE; infoPlist.NSLocalNetworkUsageDescription = props.localNetworkUsageDescription ?? infoPlist.NSLocalNetworkUsageDescription ?? LAN_USAGE; if (!Array.isArray(infoPlist.NSBonjourServices)) { infoPlist.NSBonjourServices = []; } if (!infoPlist.NSBonjourServices.includes('_http-alt._tcp.')) { infoPlist.NSBonjourServices.push('_http-alt._tcp.'); } if (!Array.isArray(infoPlist.UIBackgroundModes)) { infoPlist.UIBackgroundModes = []; } if (!infoPlist.UIBackgroundModes.includes('bluetooth-central')) { infoPlist.UIBackgroundModes.push('bluetooth-central'); } if (!infoPlist.UIBackgroundModes.includes('bluetooth-peripheral')) { infoPlist.UIBackgroundModes.push('bluetooth-peripheral'); } return config; }); }; const withDittoAndroid: any = (expoConfig: any) => { const { withBuildProperties, withAndroidManifest } = loadExpoDependencies(); const architectures = ['x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a']; const libraries = [ 'libjsi.so', 'libdittoffi.so', 'libreact_nativemodule_core.so', 'libturbomodulejsijni.so', 'libreactnative.so', ]; expoConfig = withBuildProperties(expoConfig, { android: { packagingOptions: { pickFirst: architectures.flatMap((arch: string) => libraries.map((lib) => `lib/${arch}/${lib}`) ), }, }, }); expoConfig = withAndroidManifest(expoConfig, async (config: any) => { const androidManifest = config.modResults; const permissions = androidManifest.manifest['uses-permission'] || []; const permissionsToAdd = [ { name: 'android.permission.BLUETOOTH' }, { name: 'android.permission.BLUETOOTH_ADMIN' }, { name: 'android.permission.BLUETOOTH_ADVERTISE' }, { name: 'android.permission.BLUETOOTH_CONNECT' }, { name: 'android.permission.BLUETOOTH_SCAN', attributes: { 'android:usesPermissionFlags': 'neverForLocation' }, }, { name: 'android.permission.ACCESS_FINE_LOCATION' }, { name: 'android.permission.ACCESS_COARSE_LOCATION' }, { name: 'android.permission.INTERNET' }, { name: 'android.permission.ACCESS_WIFI_STATE' }, { name: 'android.permission.ACCESS_NETWORK_STATE' }, { name: 'android.permission.CHANGE_NETWORK_STATE' }, { name: 'android.permission.CHANGE_WIFI_MULTICAST_STATE' }, { name: 'android.permission.CHANGE_WIFI_STATE' }, { name: 'android.permission.NEARBY_WIFI_DEVICES', attributes: { 'android:usesPermissionFlags': 'neverForLocation' }, }, ]; permissionsToAdd.forEach((permission) => { function isPermissionAlreadyRequested( permissionName: string, manifestPermissions: any[] ): boolean { return manifestPermissions.some( (e) => e.$['android:name'] === permissionName ); } if (!isPermissionAlreadyRequested(permission.name, permissions)) { const permissionObject = { $: { 'android:name': permission.name, ...permission.attributes, }, }; permissions.push(permissionObject); } }); androidManifest.manifest['uses-permission'] = permissions; return config; }); return expoConfig; }; // Workaround for fmt 11.x consteval incompatibility with Xcode 26.4 clang. // Compile the fmt pod as C++17 so __cpp_consteval is not defined, which forces // fmt to skip the consteval FMT_STRING code path entirely and fall back to // runtime format-string validation. Scoped to the fmt target only — RCT-Folly, // React-Core, and the rest of RN keep their C++20 settings. // See: https://github.com/fmtlib/fmt/issues/4740 and SDKS-3242. // Remove when React Native upgrades to a build that bundles fmt >= 11.1. const FMT_CONSTEVAL_SNIPPET = ` # Ditto: fmt consteval workaround for Xcode 26.4 (SDKS-3242). installer.pods_project.targets.each do |target| if target.name == 'fmt' target.build_configurations.each do |cfg| cfg.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++17' end end end `; // Idempotency marker. The comment text is stable — it comes from // FMT_CONSTEVAL_SNIPPET and we own it — so a formatter reflowing whitespace // around it won't cause a duplicate injection. const FMT_CONSTEVAL_MARKER = '# Ditto: fmt consteval workaround for Xcode 26.4 (SDKS-3242).'; // Match `react_native_post_install(...)` for both the single-line form // (`react_native_post_install(installer)`) and the multi-line form with // trailing arguments. Handles one level of balanced parens in the arg list, // which is more than any real Podfile template requires. const REACT_NATIVE_POST_INSTALL_RE = /react_native_post_install\((?:[^()]|\([^()]*\))*\)\s*/; const withDittoFmtConstevalFix: any = (expoConfig: any) => { const { withDangerousMod } = loadExpoDependencies(); return withDangerousMod(expoConfig, [ 'ios', async (config: any) => { const fs = require('node:fs'); const path = require('node:path'); const podfilePath = path.join( config.modRequest.platformProjectRoot, 'Podfile' ); if (!fs.existsSync(podfilePath)) return config; let podfile: string = fs.readFileSync(podfilePath, 'utf8'); if (podfile.includes(FMT_CONSTEVAL_MARKER)) return config; if (!REACT_NATIVE_POST_INSTALL_RE.test(podfile)) return config; podfile = podfile.replace( REACT_NATIVE_POST_INSTALL_RE, (match: string) => match + FMT_CONSTEVAL_SNIPPET ); fs.writeFileSync(podfilePath, podfile); return config; }, ]); }; const withDitto: any = (config: any, props: DittoConfig = {}) => { config = withDittoIOS(config, props); config = withDittoAndroid(config, props); config = withDittoFmtConstevalFix(config); return config; }; export default function dittoExpoPlugin(...args: any[]) { const { createRunOncePlugin } = loadExpoDependencies(); return createRunOncePlugin(withDitto, 'ditto_expo')(...args); }