I was going through my payment-related dependencies before moving to SDK 58 when one report in the Expo issue list made me stop scrolling. Stripe was in its title.
useFrameworks was already dynamic, and still pod install said Stripe was being built as a static library. When the setting you need is already there and the build still fails, you tend to look in the wrong place for a long time.
I haven't reproduced this setup myself. What follows comes from reading the issue and the comment where the Expo side reproduced it on EAS. On top of that, I wrote down a way to check whether your own project meets the conditions before you start a build.
The short version first. If you have Stripe, useFrameworks: "dynamic", and precompiled modules enabled, you are affected. There are three ways out, and the choice depends on how soon you need to ship and how much extra iOS build time you can live with.
What is happening
The report is expo/expo issue 51652. Here are the conditions.
| Item | Reported value |
|---|---|
| Expo | 58.0.11 |
| React Native | 0.88.0 |
| @stripe/stripe-react-native | 0.81.0 |
| expo-modules-autolinking | 58.0.13 |
| CocoaPods | 1.17.0 |
| expo-build-properties (ios) | useFrameworks: dynamic, usePrecompiledModules: true |
Running npx expo prebuild --platform ios --clean makes CocoaPods stop with an error along these lines.
[!] [stripe-react-native] Resolving the Stripe iOS SDK through Swift Package
Manager requires dynamic frameworks, but stripe-react-native is building as
a static library. To fix, add `use_frameworks! :linkage => :dynamic` to your PodfileThere is a trap in reading this. The message tells you to add dynamic linkage, but that setting is already in place. What is missing is not a setting. Something else overrides the result.
The reporter traced it in Ruby. precompiled_modules.rb in expo-modules-autolinking collects pods that depend on React-Core into pods_to_downgrade and swaps their build type to a static library. Stripe's Swift Package Manager integration requires dynamic, and the two collide.
The Expo side's comment reached the same reading. They ran three builds of one project on the EAS iOS preview profile.
| Stripe 0.81.0 | usePrecompiledModules | Result |
|---|---|---|
| yes | true | Failed (Install pods) |
| yes | false | Succeeded |
| no | true | Succeeded |
So the build fails only when Stripe and precompiled modules are both present. Either one alone passes. Also, the EAS screen reportedly shows only "Unknown error. See logs of the Install pods build phase". You have to open the log of that phase to see the real message.
Check your own project before building
I wrote a small script that reads app.json and package.json and judges only whether the three conditions line up. Since the documentation lists usePrecompiledModules as true by default, a missing value is counted as enabled.
// check-stripe-linkage.js — check the Stripe + precompiled modules combination before building
// Usage: node check-stripe-linkage.js [project path]
const fs = require('fs');
const path = require('path');
const root = path.resolve(process.argv[2] || '.');
const readJson = (name) => {
const p = path.join(root, name);
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null;
};
const pkg = readJson('package.json');
if (!pkg) { console.error('package.json not found: ' + root); process.exit(2); }
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
const stripeVer = deps['@stripe/stripe-react-native'];
const expoVer = deps['expo'];
// app.json comes in two shapes: { "expo": {...} } and the bare object
const app = readJson('app.json');
const cfg = app ? (app.expo || app) : null;
if (!cfg) {
const dyn = ['app.config.js', 'app.config.ts'].find((f) => fs.existsSync(path.join(root, f)));
console.log(dyn
? `⚠️ ${dyn} has to be executed to read it. Check the expo-build-properties plugin by eye`
: '⚠️ app.json not found');
process.exit(dyn ? 0 : 2);
}
// Find expo-build-properties in plugins (a bare string entry means no options)
let ios = {};
for (const pl of cfg.plugins || []) {
if (Array.isArray(pl) && pl[0] === 'expo-build-properties') ios = (pl[1] && pl[1].ios) || {};
}
const useFrameworks = ios.useFrameworks || '(not set)';
// Per the docs, usePrecompiledModules defaults to true
const precompiled = ios.usePrecompiledModules === undefined ? true : ios.usePrecompiledModules;
console.log(`expo : ${expoVer || '(none)'}`);
console.log(`@stripe/stripe-react-native: ${stripeVer || '(none)'}`);
console.log(`ios.useFrameworks : ${useFrameworks}`);
console.log(`ios.usePrecompiledModules : ${precompiled}${ios.usePrecompiledModules === undefined ? ' (default)' : ''}`);
if (!stripeVer) { console.log('✅ Stripe is not installed, so this issue does not apply'); process.exit(0); }
if (useFrameworks === 'dynamic' && precompiled !== false) {
console.log('❌ Stripe + useFrameworks: dynamic + precompiled modules on. This combination stops prebuild / pod install on SDK 58');
console.log(' → set ios.usePrecompiledModules to false, or test a build without Stripe');
process.exit(1);
}
console.log('✅ This combination does not meet the reported failure conditions');On my machine, a config with Stripe, dynamic, and no usePrecompiledModules stops with exit code 1. Setting usePrecompiledModules to false passes, and so does a setup without Stripe. What I confirmed is how this check behaves. I did not run a real pod install.
I made it return an exit code so you can put it in your package.json scripts or CI as a check before sending a build to EAS.
Three ways out
1. Turn precompiled modules off
This is the one that actually passed for both the reporter and the Expo side.
{
"expo": {
"plugins": [
[
"expo-build-properties",
{
"ios": {
"useFrameworks": "dynamic",
"usePrecompiledModules": false
}
}
]
]
}
}The cost can be read from the documentation. usePrecompiledModules switches between using prebuilt XCFrameworks and building from source. Turn it off and Expo's modules go back to building from source, so the iOS build should get heavier. I haven't measured by how much.
2. Handle it on Stripe's side
According to the Expo comment, Stripe has a way to set $StripeDisableSPM = true in the Podfile. The same comment adds that Stripe marks this as deprecated, that a managed project needs a config plugin to inject it into the Podfile, and that the Expo side didn't try it either.
I wouldn't pick this. It spends pre-release time on a road nobody has tested.
3. Wait
As of October 22 the issue is still open. The Expo side lists, as a design decision, either making precompiled modules support pods that must be dynamic, or keeping them static and printing a diagnostic. A fix on Stripe's side is mentioned as a possibility too. In other words, there is no outlook for when it gets fixed.
You can also keep shipping on SDK 57 and hold the move to 58. If this issue is your only reason to hold, watch how it moves and decide as you go.
Choosing
My line comes from two questions.
- Do you need to ship iOS this month? If yes, choose 1. Removing Stripe would stop your payments, so that's not realistic.
- Do you not need to ship this month, and have no particular reason to be on 58? Then choose 3.
For an indie developer who has added payments, a slightly heavier build costs far less than not being able to submit. Build time can be measured and trimmed later. A release window doesn't come back.
Pay for the slowness you can fix, never for the delay you can't get back. I try to keep this line even on SDK updates.
After you turn it off
Once usePrecompiledModules is false, run npx expo prebuild --platform ios --clean once and confirm pod install goes through to the end. If you build on EAS and it fails, open the log of the Install pods phase.
Two other SDK 58 checks are worth running before an upgrade: the .env.staging file that is no longer read and the permission strings that pile up in Info.plist. For wiring in Stripe itself, see Rork × Stripe subscription implementation.
Start by running check-stripe-linkage.js once and see whether your own project is affected.