Skip to content

Commit 3b9e498

Browse files
authored
Guard Nitro init; add Expo Folly opt-in (#3003)
## Summary - Guard Nitro initialization by side‑effect import and try/catch; remove direct runtime probe dependency - Add optional Expo config plugin to disable Folly coroutines via Podfile macros (default off) ## Notes - Keeps CI/Jest stable by avoiding unavailable named imports - Idempotent Podfile edit; applies only when opt‑in flag is set <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * iOS: Fixed startup crash related to in‑app purchases and sanitized error payloads to avoid leaking product IDs; listeners warn/inert until runtime initializes. * **New Features** * Plugin supports an optional Expo config flag to disable Folly coroutines in iOS builds. * **Documentation** * Clarified event‑driven purchase flow, FAQ on transient success→error timing, and guidance to prefer purchase.id; added installation workaround steps. * **Examples** * Added subscription demos, item‑detail modal, and redacted sensitive fields. * **Chores** * Dev tooling updates and prerelease version bump; added Node type definitions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 0fa3f4a commit 3b9e498

File tree

12 files changed

+13288
-5355
lines changed

12 files changed

+13288
-5355
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@
2424
"[javascriptreact]": {
2525
"editor.defaultFormatter": "esbenp.prettier-vscode"
2626
},
27-
"cSpell.words": ["hyochan", "skus"]
27+
"cSpell.words": ["couroutines", "hyochan", "Podfile", "skus"]
2828
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2121

2222
- iOS: Sanitize purchase error payload so `purchaseToken` does not carry product identifiers in error events; keep internal deduplication based on SKU
2323
- Examples/Tests: Align transaction row with `purchase.id`; update tests accordingly
24+
- iOS: Fix crash on startup by lazily initializing the IAP module (Nitro HybridObject created on demand)
2425

2526
- Remove any references to `purchase.transactionReceipt`, `purchase.transactionReceiptIOS`, and `purchase.purchaseTokenAndroid`
2627

docs/blog/2025-09-14-release-14.3.1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ authors: [hyochan]
44
tags: [release, docs, examples, ios]
55
---
66

7-
Small polish release focusing on examples, documentation, and a minor iOS error‑payload fix.
7+
Small polish release focusing on examples, documentation, and a critical fix for an iOS crash on startup.
88

99
## Added
1010

docs/docs/installation.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,39 @@ After installing the package, you need to:
7878

7979
3. **Create a development build** (see the Platform Configuration section below for details)
8080

81+
4. Optional: Fix iOS Folly coroutine include error
82+
83+
If your iOS build fails with errors such as `'folly/coro/Coroutine.h' file not found` from `RCT-Folly/folly/Expected.h`, you can opt‑in to a workaround that disables Folly coroutine support during CocoaPods install.
84+
85+
Add this flag to the `react-native-iap` plugin options in your Expo config:
86+
87+
```json
88+
{
89+
"expo": {
90+
"plugins": [
91+
[
92+
"react-native-iap",
93+
{
94+
"ios": {
95+
"with-folly-no-couroutines": true
96+
}
97+
}
98+
]
99+
]
100+
}
101+
}
102+
```
103+
104+
What this does:
105+
- Injects `FOLLY_NO_CONFIG=1`, `FOLLY_CFG_NO_COROUTINES=1`, and `FOLLY_HAS_COROUTINES=0` into the Podfile `post_install` block for all Pods targets, preventing `RCT-Folly` from including non‑vendored `<folly/coro/*>` headers.
106+
- Idempotent: skips if you already set these defines yourself.
107+
108+
After enabling the flag, re-run prebuild and install pods:
109+
- `rm -rf ios`
110+
- `npx expo prebuild -p ios`
111+
- `cd ios && LANG=en_US.UTF-8 pod install --repo-update`
112+
- `npx expo run:ios`
113+
81114
## Platform Configuration
82115

83116
### For Expo Managed Workflow

example-expo/bun.lock

Lines changed: 9821 additions & 1983 deletions
Large diffs are not rendered by default.

example-expo/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"react-dom": "19.0.0",
3838
"react-native": "0.79.6",
3939
"react-native-gesture-handler": "~2.24.0",
40-
"react-native-nitro-modules": "^0.29.2",
40+
"react-native-nitro-modules": "^0.29.4",
4141
"react-native-reanimated": "~3.17.4",
4242
"react-native-safe-area-context": "5.4.0",
4343
"react-native-screens": "~4.11.1",

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@react-navigation/native-stack": "^7.3.25",
1818
"react": "19.1.0",
1919
"react-native": "0.81.1",
20-
"react-native-nitro-modules": "^0.29.2",
20+
"react-native-nitro-modules": "^0.29.4",
2121
"react-native-safe-area-context": "^5.6.1",
2222
"react-native-screens": "^4.15.4"
2323
},

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-iap",
3-
"version": "14.3.1",
3+
"version": "14.3.2-rc.9",
44
"description": "React Native In-App Purchases module for iOS and Android using Nitro",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",
@@ -55,8 +55,8 @@
5555
"lint:tsc": "tsc -p tsconfig.json --noEmit --skipLibCheck",
5656
"lint:ci": "yarn lint:tsc && yarn lint:eslint && yarn lint:prettier",
5757
"prepare": "npx tsx scripts/check-nitro-versions.ts && bob build && yarn nitrogen && yarn build:plugin",
58-
"nitrogen": "nitro-codegen",
59-
"specs": "nitro-codegen --logLevel=\"debug\"",
58+
"nitrogen": "nitrogen",
59+
"specs": "nitrogen --logLevel=\"debug\"",
6060
"test": "jest --coverage",
6161
"test:library": "jest --coverage",
6262
"test:example": "yarn workspace rn-iap-example test --coverage",
@@ -95,6 +95,7 @@
9595
"@testing-library/react": "^16.3.0",
9696
"@testing-library/react-hooks": "^8.0.1",
9797
"@types/jest": "^29.5.12",
98+
"@types/node": "^24.4.0",
9899
"@types/react": "^19.1.12",
99100
"babel-jest": "^30.1.1",
100101
"eslint": "^8.57.0",
@@ -105,12 +106,12 @@
105106
"husky": "^8.0.3",
106107
"jest": "^30.1.1",
107108
"lint-staged": "^15.2.0",
108-
"nitro-codegen": "^0.29.2",
109+
"nitrogen": "^0.29.4",
109110
"prettier": "^3.3.3",
110111
"react": "19.1.0",
111112
"react-native": "0.81.1",
112113
"react-native-builder-bob": "^0.38.4",
113-
"react-native-nitro-modules": "^0.29.2",
114+
"react-native-nitro-modules": "^0.29.4",
114115
"react-test-renderer": "^19.1.1",
115116
"typescript": "^5.9.2"
116117
},

plugin/src/withIAP.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import {
33
WarningAggregator,
44
withAndroidManifest,
55
withAppBuildGradle,
6+
withPodfile,
67
} from 'expo/config-plugins';
78
import type {ConfigPlugin} from 'expo/config-plugins';
9+
import type {ExpoConfig} from '@expo/config-types';
810

911
const pkg = require('../../package.json');
1012

@@ -122,9 +124,67 @@ const withIapAndroid: ConfigPlugin = (config) => {
122124
return config;
123125
};
124126

125-
const withIAP: ConfigPlugin = (config, _props) => {
127+
type IapPluginProps = {
128+
ios?: {
129+
// Intentionally following user-provided key spelling
130+
// Enable to inject Folly coroutine-disabling macros into Podfile during prebuild
131+
'with-folly-no-couroutines'?: boolean;
132+
};
133+
};
134+
135+
const withIapIosFollyWorkaround: ConfigPlugin<IapPluginProps | undefined> = (
136+
config,
137+
props,
138+
) => {
139+
const enabled = !!props?.ios?.['with-folly-no-couroutines'];
140+
if (!enabled) return config;
141+
142+
return withPodfile(config, (config) => {
143+
let contents = config.modResults.contents;
144+
145+
// Idempotency: if any of the defines already exists, assume it's applied
146+
if (
147+
contents.includes('FOLLY_CFG_NO_COROUTINES') ||
148+
contents.includes('FOLLY_HAS_COROUTINES=0')
149+
) {
150+
return config;
151+
}
152+
153+
const anchor = 'post_install do |installer|';
154+
const snippet = `
155+
# react-native-iap (expo): Disable Folly coroutines to avoid including non-vendored <folly/coro/*> headers
156+
installer.pods_project.targets.each do |target|
157+
target.build_configurations.each do |config|
158+
defs = (config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] || ['$(inherited)'])
159+
defs << 'FOLLY_NO_CONFIG=1' unless defs.any? { |d| d.to_s.include?('FOLLY_NO_CONFIG') }
160+
# Portability.h respects FOLLY_CFG_NO_COROUTINES to fully disable coroutine support
161+
defs << 'FOLLY_CFG_NO_COROUTINES=1' unless defs.any? { |d| d.to_s.include?('FOLLY_CFG_NO_COROUTINES') }
162+
defs << 'FOLLY_HAS_COROUTINES=0' unless defs.any? { |d| d.to_s.include?('FOLLY_HAS_COROUTINES') }
163+
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = defs
164+
end
165+
end`;
166+
167+
if (contents.includes(anchor)) {
168+
contents = contents.replace(anchor, `${anchor}\n${snippet}`);
169+
} else {
170+
// As a fallback, append a new post_install block
171+
contents += `
172+
173+
${anchor}
174+
${snippet}
175+
end
176+
`;
177+
}
178+
179+
config.modResults.contents = contents;
180+
return config;
181+
});
182+
};
183+
184+
const withIAP: ConfigPlugin<IapPluginProps | undefined> = (config, props) => {
126185
try {
127-
const result = withIapAndroid(config);
186+
let result = withIapAndroid(config);
187+
result = withIapIosFollyWorkaround(result, props);
128188
// Set flag after first execution to prevent duplicate logs
129189
hasLoggedPluginExecution = true;
130190
return result;
@@ -138,4 +198,25 @@ const withIAP: ConfigPlugin = (config, _props) => {
138198
}
139199
};
140200

141-
export default createRunOncePlugin(withIAP, pkg.name, pkg.version);
201+
// Standard Expo config plugin export
202+
// Export a test-friendly wrapper that accepts 1 or 2 args
203+
type IapPluginCallable = {
204+
(config: ExpoConfig): ExpoConfig;
205+
(config: ExpoConfig, props?: IapPluginProps): ExpoConfig;
206+
};
207+
208+
const _wrapped = createRunOncePlugin(
209+
withIAP,
210+
pkg.name,
211+
pkg.version,
212+
) as unknown as (
213+
config: ExpoConfig,
214+
props: IapPluginProps | undefined,
215+
) => ExpoConfig;
216+
217+
const pluginExport: IapPluginCallable = ((
218+
config: ExpoConfig,
219+
props?: IapPluginProps,
220+
) => _wrapped(config, props)) as unknown as IapPluginCallable;
221+
222+
export {pluginExport as default};

0 commit comments

Comments
 (0)