Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wicked-clocks-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@callstack/react-native-brownfield': patch
---

fix: autodetect iOS app target name for Expo pre-55 patch script phase
28 changes: 18 additions & 10 deletions docs/docs/docs/getting-started/expo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PackageManagerTabs } from '@theme';
This guide walks you through packaging your Expo React Native app as an **AAR** or **XCFramework** and integrating it into your native **Android** or **iOS** application.

## Prerequisites

- Install the `@callstack/react-native-brownfield` package from the quick start [section](/docs/getting-started/quick-start#installation)

## Configuration
Expand All @@ -13,9 +14,7 @@ This guide walks you through packaging your Expo React Native app as an **AAR**

```json
{
"plugins": [
"@callstack/react-native-brownfield",
]
"plugins": ["@callstack/react-native-brownfield"]
}
```

Expand Down Expand Up @@ -55,7 +54,7 @@ This should only take a few minutes.

> That is all from the AAR steps. We can now consume the AAR inside a native Android App.

### AAR: Present RN UI
### AAR: Present RN UI

1. Call the `ReactNativeHostManager.initialize` in your Activity or Application:

Expand All @@ -80,6 +79,7 @@ override fun onConfigurationChanged(newConfig: Configuration) {
```

3. Use either of the following APIs to present the UI:

- ReactNativeFragment
- See the example [here](https://github.com/callstack/react-native-brownfield/blob/41c81059acda8b134b6fea6bbbcf918c20d16552/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt#L133)
- ReactNativeFragment.createReactNativeFragment
Expand All @@ -90,7 +90,7 @@ override fun onConfigurationChanged(newConfig: Configuration) {

4. Build and install the android application 🚀

<hr/>
<hr />

## iOS Integration

Expand All @@ -109,23 +109,23 @@ This should only take a few minutes.
##### Pre-Requisites

- Follow the step for adding the frameworks to your iOS App - [here](/docs/getting-started/ios#6-add-the-framework-to-your-ios-app)
<hr/>
<hr />

1. Call the following functions from your Application Entry point:

```swift
@main
struct IosApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

init() {
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
ReactNativeBrownfield.shared.startReactNative {
print("React Native has been loaded")
}
ReactNativeBrownfield.shared.ensureExpoModulesProvider()
}

var body: some Scene {
WindowGroup {
ContentView()
Expand All @@ -139,7 +139,7 @@ struct IosApp: App {
```swift
class AppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
Expand All @@ -160,7 +160,7 @@ ReactNativeView(moduleName: "ExpoRNApp")

4. Build and install the iOS application 🚀

<hr/>
<hr />

## Plugin Options

Expand Down Expand Up @@ -201,6 +201,14 @@ You can pass plugin options through the second item in the `plugins` tuple in `a
- `frameworkVersion` (`string`, default: `"1"`)
- Framework version used for Apple build settings (must be an integer or floating point value, for example `"1"` or `"2.1"`).

> The plugin will determine the application target automatically. The auto-detection path works as follows:
>
> 1. _Common for all cases_: scan the iOS targets for ones of type `com.apple.product-type.application`
> 2. Use the first matching strategy:
> - CNG-derived name from mod compiler (`modRequest.projectName`) - only if it exists in the filtered application-type list of Xcode project targets
> - Unambiguous first application-type target - if there is exactly one
> - PBX "first native target" fallback - last resort fallback that selects the first native target of any type

### Android

- `moduleName` (`string`, default: `"brownfieldlib"`)
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native-brownfield/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@
"access": "public"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
"@expo/config-plugins": "^54.0.4"
},
"dependencies": {
"@callstack/brownfield-cli": "workspace:^"
Expand All @@ -92,6 +91,7 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@expo/config-plugins": "^54.0.4",
"@react-native/babel-preset": "0.82.1",
"@types/jest": "^30.0.0",
"@types/react": "^19.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const withBrownfieldIos: ConfigPlugin<
? parseInt(config.sdkVersion.split('.')[0], 10)
: -1;
const isExpoPre55 = expoMajor < 55;

// Step 1: modify the Xcode project to add framework target &
config = withXcodeProject(config, (xcodeConfig) => {
const { modResults: project, modRequest } = xcodeConfig;
Expand Down Expand Up @@ -60,7 +59,7 @@ export const withBrownfieldIos: ConfigPlugin<
`Adding ExpoModulesProvider patch phase for Expo SDK ${config.sdkVersion}`
);

addExpoPre55ShellPatchScriptPhase(project, {
addExpoPre55ShellPatchScriptPhase(modRequest, project, {
frameworkName: props.ios.frameworkName,
frameworkTargetUUID: frameworkTargetUUID,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import path from 'node:path';

import type { ModProps, XcodeProject } from '@expo/config-plugins';
import {
type ModProps,
type XcodeProject,
IOSConfig,
} from '@expo/config-plugins';

import { Logger } from '../logging';
import type { ResolvedBrownfieldPluginIosConfig } from '../types';
Expand Down Expand Up @@ -286,7 +290,66 @@ export function copyBundleReactNativePhase(
}
}

function resolveAppTargetName(
project: XcodeProject,
modRequest: ModProps<XcodeProject>
): string | null {
const appTargets = IOSConfig.Target.getNativeTargets(project)
.map(([, target]) => {
if (
!IOSConfig.Target.isTargetOfType(
target,
IOSConfig.Target.TargetType.APPLICATION
)
) {
return null;
}

const name = IOSConfig.XcodeUtils.unquote(target.name ?? '').trim();

return name ?? null;
})
.filter((name): name is string => !!name);

// 1) Unambiguous first application-type target
if (appTargets.length === 1) {
return appTargets[0];
} else {
Logger.logWarning(
'Multiple application targets found in the Xcode project. Falling back to the CNG-derived name from mod compiler.'
);
}

// 2) CNG-derived name from mod compiler (`modRequest.projectName`) - only if it exists in the filtered application-type list of Xcode project targets
const cngDerivedProjectName = modRequest.projectName;
if (cngDerivedProjectName && appTargets.includes(cngDerivedProjectName)) {
return cngDerivedProjectName;
} else {
Logger.logWarning(
'CNG-derived name from mod compiler is not set or is not an application target. Falling back to the unfiltered-type target name.'
);
}

// 3) PBX "first native target" fallback
try {
const [, firstAppTarget] = IOSConfig.Target.findFirstNativeTarget(project);
const name = IOSConfig.XcodeUtils.unquote(firstAppTarget.name ?? '').trim();
return name || null;
} catch {
Logger.logWarning(
'No first native target of any type found in the Xcode project. This was the last resort fallback.'
);
}

Logger.logError(
`Could not determine the iOS app target name from the Xcode project. Please adjust your Xcode project to have exactly one application target.`
);

return null;
}

export function addExpoPre55ShellPatchScriptPhase(
modRequest: ModProps<XcodeProject>,
project: XcodeProject,
{
frameworkName,
Expand All @@ -296,6 +359,16 @@ export function addExpoPre55ShellPatchScriptPhase(
frameworkTargetUUID: string;
}
) {
const resolvedAppTargetName = resolveAppTargetName(project, modRequest);

Logger.logInfo(`Resolved iOS app target name: ${resolvedAppTargetName}`);

if (!resolvedAppTargetName) {
throw new SourceModificationError(
`Could not determine the iOS app target name from the Xcode project.`
);
}

project.addBuildPhase(
[
// no associated files
Expand All @@ -306,6 +379,7 @@ export function addExpoPre55ShellPatchScriptPhase(
{
shellPath: '/bin/sh',
shellScript: renderTemplate('ios', 'patchExpoPre55.sh', {
'{{APP_TARGET_NAME}}': resolvedAppTargetName,
'{{FRAMEWORK_NAME}}': frameworkName,
}),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Patch by @hurali97, source: https://github.com/callstackincubator/rock/issues/492#issuecomment-3225109837
# Applicable only to Expo SDK versions prior to 55, which made ExpoModulesProvider internal by default: https://github.com/expo/expo/pull/42317
# Path to ExpoModulesProvider.swift
FILE="${SRCROOT}/Pods/Target Support Files/Pods-ExpoApp-{{FRAMEWORK_NAME}}/ExpoModulesProvider.swift"
FILE="${SRCROOT}/Pods/Target Support Files/Pods-{{APP_TARGET_NAME}}-{{FRAMEWORK_NAME}}/ExpoModulesProvider.swift"

if [ -f "$FILE" ]; then
echo "Patching $FILE to hide Expo from public interface"
Expand Down
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2420,6 +2420,7 @@ __metadata:
"@babel/preset-env": "npm:^7.25.3"
"@babel/runtime": "npm:^7.25.0"
"@callstack/brownfield-cli": "workspace:^"
"@expo/config-plugins": "npm:^54.0.4"
"@react-native/babel-preset": "npm:0.82.1"
"@types/jest": "npm:^30.0.0"
"@types/react": "npm:^19.1.1"
Expand All @@ -2432,8 +2433,7 @@ __metadata:
react-native-builder-bob: "npm:^0.40.17"
typescript: "npm:5.9.3"
peerDependencies:
react: "*"
react-native: "*"
"@expo/config-plugins": ^54.0.4
bin:
brownfield: lib/commonjs/scripts/brownfield.js
languageName: unknown
Expand Down
Loading