From 48d70bd76297e48858e375237093a9e2270eb5d8 Mon Sep 17 00:00:00 2001 From: Nathan Ahn Date: Sat, 7 Mar 2026 13:38:14 -0500 Subject: [PATCH 1/2] Initializing version 0.8.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d190781..2966f9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-app-clip", - "version": "0.7.1", + "version": "0.8.0-beta.1", "description": "Config plugin to add an App Clip to a React Native iOS app", "main": "build/index.js", "types": "build/index.d.ts", From 80064d4cb421e3e897a08c90cde205eae0dc6b55 Mon Sep 17 00:00:00 2001 From: Trent Rand Date: Sat, 7 Mar 2026 11:39:34 -0700 Subject: [PATCH 2/2] Fix package exclusion by moving App Clip target to top level (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix package exclusion with sibling targets and xcconfig flag stripping Previously, the App Clip target was generated nested inside the main app target in the Podfile. This caused it to inherit all pods from the parent, rendering `excludedPackages` completely ineffective. This change moves the App Clip target to the top level as a sibling of the main target, preventing implicit dependency inheritance. However, even with the target un-nested, CocoaPods still emits linker flags for all resolved pods into the generated `.xcconfig` files. This is especially problematic under new architecture + static frameworks, where Codegen headers are expected to exist for any linked package — causing "file not found" build failures. To address this, a `post_install` hook is injected that: 1. Removes excluded packages from the App Clip target's dependency graph 2. Strips all corresponding linker flags (`-l"Pkg"`, `-framework "Pkg"`) directly from the generated xcconfig files via `Xcodeproj::Config` This two-pronged approach ensures excluded packages are absent from both the build graph and the linker invocation, regardless of project settings. Fixes #81, #79 * Add documentation about `excludedPackages` option * Remove post_install xcconfig stripping (unnecessary after target un-nesting) Once the App Clip target is a sibling rather than nested inside the main target, `use_expo_modules!(exclude:)` fully prevents excluded packages from being installed or linked. The `post_install` hook that manually stripped OTHER_LDFLAGS was compensating for inherited linker flags that no longer exist. * Append target to end of Podfile without anchor Rather than relying on an anchor that may change with later versions of Expo, we now just append the App Clip target to the end of the Podfile. This also removes dependency on Expo 53+. --- README.md | 16 ++++-- plugin/src/withPodfile.ts | 103 ++++++++++++++++++-------------------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index cfac889..1224871 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # react-native-app-clip -> **Warning** -> Starting with version 0.6.0, react-native-app-clip requires **Expo SDK 53** and **React Native 0.79**. Downgrade to 0.5.1 if you wish to use **Expo SDK 52** and **React Native 0.76**. - Expo Config Plugin that generates an App Clip for iOS apps built with Expo. ## Installation @@ -58,9 +55,9 @@ NOTE: You can find the simulator device UUID by running `xcrun simctl list`. The - **requestLocationConfirmation** (boolean): Allow App Clip access to location data (see [Apple Developer Docs](https://developer.apple.com/documentation/app_clips/confirming_the_user_s_physical_location)) - **appleSignin** (boolean): Enable "Sign in with Apple" for the App Clip - **applePayMerchantIds** (string[]): Enable Apple Pay capability with provided merchant IDs. -- **excludedPackages** (string[]): Packages to exclude from autolinking for the App Clip to reduce bundle size (see below). - **pushNotifications** (boolean): Enable push notification compatibility for the App Clip - **enableCompression** (boolean): Enables gzip compression of the App Clip's JavaScript bundle to reduce its size. Please note: This may increase the final binary size in some cases (see [App Clip Size Limits](#app-clip-size-limits)). +- **excludedPackages** (string[]): node module names to exclude from autolinking for the App Clip to reduce binary size (see [App Clip Size Limits](#app-clip-size-limits)). ## App Clip Size Limits @@ -76,7 +73,16 @@ For iOS 17+, the 100 MB limit has additional requirements: - Requires reliable internet connection usage scenarios - Does not support iOS 16 and earlier -You can exclude packages (via `excludedPackages` parameter) and use compression (via `enableCompression` parameter) to help stay within these limits. However, since the App Clip binary itself is compressed by Apple, pre-compressing the JS bundle with `enableCompression` might sometimes be counterproductive. Always verify the final size in TestFlight or the App Store Connect dashboard. +You can exclude packages (via `excludedPackages`) and use compression (via `enableCompression`) to help stay within these limits. However, since the App Clip binary itself is compressed by Apple, pre-compressing the JS bundle with `enableCompression` might sometimes be counterproductive. Always verify the final size in TestFlight or the App Store Connect dashboard. + +Excluded packages are removed from Expo's autolinking for the App Clip target via `use_expo_modules!`. Use node module package names: + +```json +"excludedPackages": [ + "expo-notifications", + "react-native-nfc-manager" +] +``` ## Native capabilities diff --git a/plugin/src/withPodfile.ts b/plugin/src/withPodfile.ts index db38328..1da12a9 100644 --- a/plugin/src/withPodfile.ts +++ b/plugin/src/withPodfile.ts @@ -1,4 +1,3 @@ -import { mergeContents } from "@expo/config-plugins/build/utils/generateCode"; import { type ConfigPlugin, withDangerousMod } from "expo/config-plugins"; import fs from "node:fs"; import path from "node:path"; @@ -7,8 +6,6 @@ export const withPodfile: ConfigPlugin<{ targetName: string; excludedPackages?: string[]; }> = (config, { targetName, excludedPackages }) => { - // return config; - return withDangerousMod(config, [ "ios", (config) => { @@ -20,66 +17,66 @@ export const withPodfile: ConfigPlugin<{ const useExpoModules = excludedPackages && excludedPackages.length > 0 - ? `exclude = ["${excludedPackages.join(`", "`)}"]\n use_expo_modules!(exclude: exclude)` + ? `exclude = ["${excludedPackages.join(`", "`)}"]\n use_expo_modules!(exclude: exclude)` : "use_expo_modules!"; const appClipTarget = ` - target '${targetName}' do - ${useExpoModules} - - if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' - config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; - else - config_command = [ - 'npx', - 'expo-modules-autolinking', - 'react-native-config', - '--json', - '--platform', - 'ios' - ] - end +# @generated begin react-native-app-clip +target '${targetName}' do + ${useExpoModules} - # Running the command in the same manner as \`use_react_native\` then running that result through our cliPlugin - json, message, status = Pod::Executable.capture_command(config_command[0], config_command[1..], capture: :both) - if not status.success? - Pod::UI.warn "The command: '#{config_command.join(" ").bold.yellow}' returned a status code of #{status.exitstatus.to_s.bold.red}, #{message}", [ - "App Clip autolinking failed. Please ensure autolinking works correctly for the main app target and try again.", - ] - exit(status.exitstatus) - end + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' + config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; + else + config_command = [ + 'npx', + 'expo-modules-autolinking', + 'react-native-config', + '--json', + '--platform', + 'ios' + ] + end - # \`react-native-app-clip\` resolves to react-native-app-clip/build/index.js - clip_command = [ - 'node', - '--no-warnings', - '--eval', - 'require(require.resolve(\\'react-native-app-clip\\')+\\'/../../plugin/build/cliPlugin.js\\').run(' + json + ', [${(excludedPackages ?? []).map((packageName) => `"${packageName}"`).join(", ")}])' + # Running the command in the same manner as \`use_react_native\` then running that result through our cliPlugin + json, message, status = Pod::Executable.capture_command(config_command[0], config_command[1..], capture: :both) + if not status.success? + Pod::UI.warn "The command: '#{config_command.join(" ").bold.yellow}' returned a status code of #{status.exitstatus.to_s.bold.red}, #{message}", [ + "App Clip autolinking failed. Please ensure autolinking works correctly for the main app target and try again.", ] + exit(status.exitstatus) + end - config = use_native_modules!(clip_command) + # \`react-native-app-clip\` resolves to react-native-app-clip/build/index.js + clip_command = [ + 'node', + '--no-warnings', + '--eval', + 'require(require.resolve(\\'react-native-app-clip\\')+\\'/../../plugin/build/cliPlugin.js\\').run(' + json + ', [])' + ] - use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] - use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] + config = use_native_modules!(clip_command) - use_react_native!( - :path => config[:reactNativePath], - :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', - # An absolute path to your application root. - :app_path => "#{Pod::Config.instance.installation_root}/..", - :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', - ) - end - `; + use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] + use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] - podfileContent = mergeContents({ - tag: "Generated by react-native-app-clip", - src: podfileContent, - newSrc: appClipTarget, - anchor: "use_expo_modules!", - offset: 0, - comment: "#", - }).contents; + use_react_native!( + :path => config[:reactNativePath], + :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/..", + :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', + ) +end +# @generated end react-native-app-clip`; + + // Strip any existing block then re-append at end of file (idempotent) + const blockRegex = new RegExp( + `\\n*# @generated begin react-native-app-clip[\\s\\S]*?# @generated end react-native-app-clip`, + "g", + ); + podfileContent = podfileContent.replace(blockRegex, "").trimEnd(); + podfileContent += `\n${appClipTarget}\n`; fs.writeFileSync(podFilePath, podfileContent);