Skip to content

fix(ios): support static SPM with React Native shared products - #2757

Open
hknakn wants to merge 1 commit into
stripe:masterfrom
hknakn:hknakn/expo-precompiled-static-spm
Open

hknakn wants to merge 1 commit into
stripe:masterfrom
hknakn:hknakn/expo-precompiled-static-spm

Conversation

@hknakn

@hknakn hknakn commented Oct 20, 2026 •

Copy link
Copy Markdown

Summary

Support static Stripe SPM consumers with React Native's shared build products.

Problem

Expo precompiled modules make Stripe's adapter static. Stripe rejects this configuration even when React Native supports it.

Solution

Accept static libraries configured with shared build products. Preserve dynamic-framework support and reject unsupported configurations.

Motivation

Expo SDK 58 precompiled modules make React-Core consumers static, including stripe-react-native. Stripe currently rejects that target before compilation, even when React Native has configured static SPM support. This prevents consumers from enabling Expo precompiled modules.

Related report: expo/expo#51652

Static support also requires react/react-native#59182. React Native currently reconstructs module-map paths from the pod name rather than the module name; Stripe's pod and module names differ. Keep this dependency in mind before merging or releasing static support.

The check uses the generated native target's build settings instead of assuming that a React Native version includes the required support. Every configuration must use ${PODS_CONFIGURATION_BUILD_DIR}.

Testing

  • I tested this manually

  • I added automated tests

  • ruby scripts/test/stripe_spm_test.rb: seven tests, 17 assertions, no failures. The static-library case failed before the change.

  • Fresh Expo 58.0.11 / React Native 0.88.0 reproduction with useFrameworks: "dynamic" and usePrecompiledModules: true: the original Stripe 0.81.0 package fails pod installation with the dynamic-linkage error; the candidate package from this branch completes pod installation.

  • The Stripe candidate alone completes pod installation but exposes React Native's module-map path bug during compilation. With the existing React Native PR's helper at 6569aeb9ca9735aed3a008a9f28703a8caa64cad loaded in the isolated reproduction, the ARM64 iOS Simulator Debug build succeeds with precompiled modules enabled.

  • ruby spm-test.rb in the React Native PR: nine tests, 25 assertions, no failures.

  • On an iPhone 18 Pro Max simulator, initStripe resolves successfully and the native CardField renders its number, expiry and CVC fields. Entering 4242424242424242 displays Visa. Complete card entry, Release archives and payment transactions have not been tested.

Documentation

  • I have added relevant documentation for my changes.
  • This PR does not result in any developer-facing changes.

CONTRIBUTING.md describes supported linkage and the required react_native_post_install call.

Testing directions

  • Behind a flag: no
  • Requires QA: yes
  • How a reviewer can test: Run ruby scripts/test/stripe_spm_test.rb. Build an Expo SDK 58 app with usePrecompiledModules: true and the React Native prerequisite; verify initialization and native CardField rendering.

@cla-assistant

cla-assistant Bot commented Oct 20, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@hknakn
hknakn marked this pull request as ready for review October 21, 2026 09:05
@hknakn
hknakn requested review from a team as code owners October 21, 2026 09:05
chrfalch added a commit to expo/expo that referenced this pull request Oct 22, 2026
…modules (#52017)

# Why

Fixes #51652.

With `useFrameworks: "dynamic"` and precompiled modules on, `pod
install` fails for apps that use `@stripe/stripe-react-native` 0.81.0.
Expo builds every pod that depends on React-Core as a static library.
Stripe gets its iOS SDK through React Native's `spm_dependency`, and its
own post-install check stops the install unless its pod is a dynamic
framework. Stripe's error message tells the user to turn on dynamic
frameworks, which they already did. Today the only workaround is to turn
off precompiled modules.

# How

Expo now records which pods call React Native's `spm_dependency`. With
dynamic linkage, it keeps those pods and all their dependencies out of
the static downgrade.

- **Recording.** React Native's `SPMManager` has no public reader for
these pods. `use_expo_modules!` prepends a small module on
`SPMManager#dependency`, which records the root pod name and calls
`super`. It does nothing if `SPMManager` is not loaded, and it does not
read React Native's private state.
- **Whole dependency graph.** CocoaPods rejects a dynamic pod with any
static recursive dependency
(`verify_no_static_framework_transitive_dependencies`). So the pod and
every one of its `recursive_dependent_targets` keep their linkage.
Keeping only the unbuilt dependencies failed validation on
`ReactCodegen`.
- **Core pods still win.** `framework_modules_to_patch`
(ExpoModulesCore, ExpoModulesJSI, Expo, ReactAppDependencyProvider,
expo-dev-menu, plus `forceStaticLinking`) is forced static again later
in `installer.rb`. If an `spm_dependency` pod's graph contains one of
these, Expo keeps today's result for that pod and prints which pod
blocks it. Such pods work today, because React Native's SPM support
handles static pods, so this avoids a regression.
- **Scope.** This applies only with `:dynamic` linkage. With no
`spm_dependency` pods, or with static linkage, the downgrade set is the
same as before.
- **Refactor for testing.** The downgrade decision moved into
`pods_to_downgrade(pod_targets, linkage:, framework_modules_to_patch:,
spm_dependency_pods:)`, so it can be tested with fake targets.
- **@react-native-firebase compatibility (second commit).**
`@react-native-firebase` 27.0.0 is also SPM-only and calls
`spm_dependency`. It has its own post-install workaround for precompiled
modules (`rnfirebase_restore_dynamic_linkage_after_expo_prebuilt!`).
That workaround calls `Pod::Target#build_type` on every RNFB pod, and
CocoaPods keeps that method private. It only worked because Expo's
downgrade defined a public `build_type` on each downgraded pod. With the
first commit alone, RNFB pods stay dynamic and `pod install` stops with
`NoMethodError: private method 'build_type'`. That would break Firebase
apps that install today. Expo now gives every pod target that it does
not downgrade a public `build_type` that returns the real value through
`super`. RNFB's workaround then sees its pods as dynamic and does
nothing.

Stripe has a related open PR, stripe/stripe-react-native#2757. It lets
Stripe accept a static pod, but it also needs react/react-native#59182,
which is not in RN 0.88.0. Stripe still accepts a dynamic pod, so the
two changes do not conflict.

In the Stripe app, the downgrade list goes from 85 pods to 25.
`ReactCodegen` is now a dynamic framework in that app. Stripe's SPM
products link statically into `stripe_react_native.framework`.

# Test Plan

**Unit tests.** New file:
`e2e/__tests__/precompiled-pods-to-downgrade-test.ts`, 10 tests. It runs
the real `precompiled_modules.rb` in Ruby with fake pod targets. Before
the first commit, 8 of 9 failed. The `build_type` test failed before the
second commit with the same private-method error that RNFB hits. All 10
pass on Ruby 3.3.6 and on the system Ruby 2.6.10. The reviewer also
removed parts of the fix on purpose: removing the core-pod check makes 2
tests fail, removing the dynamic-only check makes 1 fail, and removing
the public `build_type` makes 1 fail.

```
jest --no-watchman e2e/__tests__/precompiled-pods-to-downgrade-test.ts
Tests: 10 passed, 10 total
```

**Repro app.** The app comes from
https://github.com/glowcore/expo-58-stripe-spm-repro, with
`expo-updates` added. Versions: SDK 58, RN 0.88.0, Stripe 0.81.0,
CocoaPods 1.16.2, Xcode 27.1. The fix was applied to the app's
`node_modules`.

| Check | Without fix | With fix |
|---|---|---|
| `pod install` | fails: "requires dynamic frameworks, but
stripe-react-native is building as a static library" | passes, `Keeping
USE_FRAMEWORKS for ... (stripe-react-native)` |
| Debug simulator build | – | succeeds |
| Release simulator build (earlier version of the change) | – | succeeds
|
| Debug device build (`CODE_SIGNING_ALLOWED=NO`) | – | succeeds |
| Release archive (`CODE_SIGNING_ALLOWED=NO`) | – | succeeds.
`stripe_react_native.framework` is dynamic, and `otool -L` shows only
`@rpath`/system paths |
| Launch on the iOS simulator | – | renders. `StripeSdk` is present,
`initialise` resolves, `CardField` is registered. No dyld errors and no
duplicate classes |

The build, archive and launch rows ran before the second commit. With
the second commit, `pod install` passes again in this app.

**Second app: Stripe + Firebase + Sentry.** The same app with
`@react-native-firebase/app` and `/messaging` 27.0.0 (published code,
unchanged) and `@sentry/react-native` 8.28.0.

| Check | Result |
|---|---|
| `pod install`, no fix | fails: Stripe's dynamic-framework error |
| `pod install`, first commit only | fails: RNFB `NoMethodError: private
method 'build_type'` |
| `pod install`, both commits | passes, `Keeping USE_FRAMEWORKS for ...
(RNFBApp, RNFBMessaging, stripe-react-native)`. `RNSentry` stays static,
as before |
| Debug simulator build, both commits (earlier form of the second
commit) | succeeds |
| `pod install` with precompiled modules off | passes |

**Not tested:** running a Release build, running on a physical device,
launching the Firebase app, and CocoaPods 1.17.0.

**Local package suite.** 18 tests in `e2e/monorepo-test.ts` fail in my
worktree, because they need the package's `build/`, which is not there.
The change touches only Ruby and adds a test file.

# Checklist

- [x] I added a `changeset`
(`.changeset/autolinking-keep-spm-dependency-pods-dynamic.md`).
- [ ] `et check-packages` passes. Not run yet; see the note above.
- [x] This diff works with `npx expo prebuild` (tested in the repro
app).
- [x] Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant