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
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
103 changes: 50 additions & 53 deletions plugin/src/withPodfile.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,8 +6,6 @@ export const withPodfile: ConfigPlugin<{
targetName: string;
excludedPackages?: string[];
}> = (config, { targetName, excludedPackages }) => {
// return config;

return withDangerousMod(config, [
"ios",
(config) => {
Expand All @@ -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);

Expand Down