diff --git a/.gitignore b/.gitignore index ddb00151..e49b359b 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,8 @@ yarn-error.log example/ios/ example/android/ example/google-services.json +.yarn/install-state.gz +example/.yarn/install-state.gz # Coverage -coverage/ \ No newline at end of file +coverage/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 56520e34..2e7e54b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +and this project adheres to [Semantic +Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] - 2026-07-01 + +### Changed +- Updated `expo` to `^55.0.0` (Expo SDK 55) +- Updated `react` to `19.2.0` +- Updated `react-native` to `0.83.2` +- Updated `@iterable/react-native-sdk` peer dependency to `^3.0.0` +- Updated minimum Node.js version to `20.19.4` + +### Notes +- Expo SDK 55 requires React Native's New Architecture +- Tested with Expo SDK 55 ## [1.0.1] - 2025-07-16 diff --git a/example/README.md b/example/README.md index 02d77b47..0e2b8519 100644 --- a/example/README.md +++ b/example/README.md @@ -97,6 +97,13 @@ npx expo run:android - Check that `google-services.json` is properly placed (Android) - Verify certificates and provisioning profiles (iOS) +4. **iOS build fails on `fmt` / `FMT_STRING` / `library 'fmt' not found` (Xcode 26.4+)** + - This is a known React Native + Xcode 26.4 compatibility issue + - `@iterable/expo-plugin` injects a Podfile workaround automatically during prebuild + - Rebuild native code: `npx expo prebuild --clean`, then `cd ios && pod install && cd ..` + - If you still see the error, confirm your generated `ios/Podfile` contains the + `@iterable/expo-plugin: fmt workaround for Xcode 26.4` comment inside `post_install` + ### Development Tips - Use `yarn start` to start the Metro bundler diff --git a/example/package.json b/example/package.json index 10909d90..103dfbd4 100644 --- a/example/package.json +++ b/example/package.json @@ -11,7 +11,7 @@ "open:android": "open -a \"Android Studio\" android" }, "dependencies": { - "@iterable/react-native-sdk": "^2.0.2", + "@iterable/react-native-sdk": "^3.0.0", "@react-navigation/native": "^7.0.19", "@react-navigation/stack": "^7.2.3", "expo": "^53.0.19", diff --git a/example/yarn.lock b/example/yarn.lock index a4036ab6..0d96aff0 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1481,20 +1481,19 @@ __metadata: languageName: node linkType: hard -"@iterable/react-native-sdk@npm:^2.0.2": - version: 2.0.2 - resolution: "@iterable/react-native-sdk@npm:2.0.2" +"@iterable/react-native-sdk@npm:^3.0.0": + version: 3.0.0 + resolution: "@iterable/react-native-sdk@npm:3.0.0" peerDependencies: "@react-navigation/native": "*" react: "*" react-native: "*" react-native-safe-area-context: "*" - react-native-vector-icons: "*" react-native-webview: "*" peerDependenciesMeta: expo: optional: true - checksum: 10c0/d1d6485dd3b982642df9569a4793aae0ff71191f8f6faffadbb580594db8e9cf2089f8a9b668d09c7b53e93ecbcd9d054833a8b82e0bb2a1d0d9a68a269d5967 + checksum: 10c0/986244429e63646cd641a7f7be1a7c93be05f12000d80d1fa5cebce29bc4d87566e72766d1a3f57fe7f5c0a579a8c7d4b5b238cc815c8be161aef0a022314cfc languageName: node linkType: hard @@ -3309,7 +3308,7 @@ __metadata: resolution: "expo-plugin-example@workspace:." dependencies: "@babel/core": "npm:^7.25.2" - "@iterable/react-native-sdk": "npm:^2.0.2" + "@iterable/react-native-sdk": "npm:^3.0.0" "@react-navigation/native": "npm:^7.0.19" "@react-navigation/stack": "npm:^7.2.3" "@types/react": "npm:~19.0.10" diff --git a/package.json b/package.json index 0855d500..40fb8571 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "main": "build/withIterable.js", "types": "build/withIterable.d.ts", "scripts": { - "build": "expo-module build", + "build": "expo-module build && yarn build:plugin", + "build:plugin": "tsc -p plugin", "clean": "expo-module clean", "lint": "expo-module lint", "test": "expo-module test", "test:coverage": "jest test --coverage --config jest.config.js", - "prepare": "expo-module prepare", + "prepare": "expo-module prepare && yarn build:plugin", "prepublishOnly": "expo-module prepublishOnly", "expo-module": "expo-module", "open:ios": "xed example/ios", @@ -36,7 +37,7 @@ "devDependencies": { "@commitlint/config-conventional": "^19.6.0", "@evilmartians/lefthook": "^1.5.0", - "@iterable/react-native-sdk": "^2.0.2", + "@iterable/react-native-sdk": "^3.0.0", "@react-native/eslint-config": "^0.79.5", "@release-it/conventional-changelog": "^9.0.2", "@types/jest": "^29.5.14", @@ -60,7 +61,7 @@ "typescript": "^5.3.3" }, "peerDependencies": { - "@iterable/react-native-sdk": "^2.0.2", + "@iterable/react-native-sdk": "^3.0.0", "expo": "*", "react": "*", "react-native": "*" diff --git a/plugin/__tests__/withIosFmtWorkaround.test.ts b/plugin/__tests__/withIosFmtWorkaround.test.ts new file mode 100644 index 00000000..0c6c54d4 --- /dev/null +++ b/plugin/__tests__/withIosFmtWorkaround.test.ts @@ -0,0 +1,55 @@ +import { + createMockPodfileConfig, + createTestConfig, + type WithIterableResult, +} from '../__mocks__'; +import { FMT_WORKAROUND_MARKER } from '../src/withIosFmtWorkaround'; +import withIterable from '../src/withIterable'; + +const EXPO_PODFILE_SNIPPET = ` + post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', + ) + end +`; + +describe('withIosFmtWorkaround', () => { + it('injects the fmt workaround after react_native_post_install', async () => { + const result = withIterable(createTestConfig(), {}) as WithIterableResult; + const modifiedPodfile = await result.mods.ios.podfile( + createMockPodfileConfig({ contents: EXPO_PODFILE_SNIPPET }) + ); + + expect(modifiedPodfile.modResults.contents).toContain( + FMT_WORKAROUND_MARKER + ); + expect(modifiedPodfile.modResults.contents).toContain( + "config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++17'" + ); + }); + + it('does not inject the workaround twice', async () => { + const result = withIterable(createTestConfig(), {}) as WithIterableResult; + const firstPass = await result.mods.ios.podfile( + createMockPodfileConfig({ contents: EXPO_PODFILE_SNIPPET }) + ); + const secondPass = await result.mods.ios.podfile( + createMockPodfileConfig({ contents: firstPass.modResults.contents }) + ); + + const markerCount = ( + secondPass.modResults.contents.match( + new RegExp( + FMT_WORKAROUND_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), + 'g' + ) + ) || [] + ).length; + + expect(markerCount).toBe(1); + }); +}); diff --git a/plugin/src/withIosFmtWorkaround.ts b/plugin/src/withIosFmtWorkaround.ts new file mode 100644 index 00000000..0911fcc2 --- /dev/null +++ b/plugin/src/withIosFmtWorkaround.ts @@ -0,0 +1,55 @@ +import { ConfigPlugin, withPodfile } from 'expo/config-plugins'; + +/** Marker comment used to avoid injecting the workaround more than once. */ +export const FMT_WORKAROUND_MARKER = + '# @iterable/expo-plugin: fmt workaround for Xcode 26.4'; + +/** + * Compiles the fmt pod in C++17 mode so FMT_USE_CONSTEVAL stays disabled. + * Required for Xcode 26.4+ when building React Native 0.79+. + * + * @see https://github.com/Iterable/react-native-sdk/blob/main/example/ios/Podfile + */ +const FMT_WORKAROUND_RUBY = ` + ${FMT_WORKAROUND_MARKER} + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + # fmt/base.h unconditionally redefines FMT_USE_CONSTEVAL based on __cplusplus, + # so preprocessor defines are overwritten. The reliable fix is to compile fmt + # in C++17 mode: FMT_CPLUSPLUS (201703L) < 201709L → FMT_USE_CONSTEVAL = 0. + # All other pods stay in C++20 (React-perflogger needs std::unordered_map::contains). + if target.name == 'fmt' + config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++17' + end + end + end`; + +export const withIosFmtWorkaround: ConfigPlugin = (config) => { + return withPodfile(config, (newConfig) => { + const { contents } = newConfig.modResults; + + if (contents.includes(FMT_WORKAROUND_MARKER)) { + return newConfig; + } + + const postInstallMatch = contents.match( + /(react_native_post_install\([\s\S]*?\n\s*\))/ + ); + + if (!postInstallMatch) { + console.warn( + '@iterable/expo-plugin: Could not inject fmt workaround into Podfile — react_native_post_install block not found' + ); + return newConfig; + } + + newConfig.modResults.contents = contents.replace( + postInstallMatch[0], + `${postInstallMatch[0]}${FMT_WORKAROUND_RUBY}` + ); + + return newConfig; + }); +}; + +export default withIosFmtWorkaround; diff --git a/plugin/src/withIterable.ts b/plugin/src/withIterable.ts index 30ae287c..0a9ed424 100644 --- a/plugin/src/withIterable.ts +++ b/plugin/src/withIterable.ts @@ -1,6 +1,7 @@ import { ConfigPlugin, withPlugins } from 'expo/config-plugins'; import { withDeepLinks } from './withDeepLinks'; +import { withIosFmtWorkaround } from './withIosFmtWorkaround'; import { type ConfigPluginProps, type ConfigPluginPropsWithDefaults, @@ -24,6 +25,7 @@ const withIterable: ConfigPlugin = (config, props = {}) => { [withStoreConfigValues, propsWithDefaults], [withPushNotifications, propsWithDefaults], [withDeepLinks, propsWithDefaults], + withIosFmtWorkaround, ]); }; diff --git a/yarn.lock b/yarn.lock index abcae090..3ba03e1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2669,7 +2669,7 @@ __metadata: dependencies: "@commitlint/config-conventional": "npm:^19.6.0" "@evilmartians/lefthook": "npm:^1.5.0" - "@iterable/react-native-sdk": "npm:^2.0.2" + "@iterable/react-native-sdk": "npm:^3.0.0" "@react-native/eslint-config": "npm:^0.79.5" "@release-it/conventional-changelog": "npm:^9.0.2" "@types/jest": "npm:^29.5.14" @@ -2692,7 +2692,7 @@ __metadata: release-it: "npm:^17.10.0" typescript: "npm:^5.3.3" peerDependencies: - "@iterable/react-native-sdk": ^2.0.2 + "@iterable/react-native-sdk": ^3.0.0 expo: "*" react: "*" react-native: "*" @@ -2702,20 +2702,19 @@ __metadata: languageName: unknown linkType: soft -"@iterable/react-native-sdk@npm:^2.0.2": - version: 2.0.2 - resolution: "@iterable/react-native-sdk@npm:2.0.2" +"@iterable/react-native-sdk@npm:^3.0.0": + version: 3.0.0 + resolution: "@iterable/react-native-sdk@npm:3.0.0" peerDependencies: "@react-navigation/native": "*" react: "*" react-native: "*" react-native-safe-area-context: "*" - react-native-vector-icons: "*" react-native-webview: "*" peerDependenciesMeta: expo: optional: true - checksum: 10c0/d1d6485dd3b982642df9569a4793aae0ff71191f8f6faffadbb580594db8e9cf2089f8a9b668d09c7b53e93ecbcd9d054833a8b82e0bb2a1d0d9a68a269d5967 + checksum: 10c0/986244429e63646cd641a7f7be1a7c93be05f12000d80d1fa5cebce29bc4d87566e72766d1a3f57fe7f5c0a579a8c7d4b5b238cc815c8be161aef0a022314cfc languageName: node linkType: hard