From 4e1d2977fdba9f75187e4b150287b56a00dc930c Mon Sep 17 00:00:00 2001 From: Kyrylo Orlov Date: Tue, 20 Oct 2026 12:31:58 -0400 Subject: [PATCH 1/4] Build config plugin before publishing expo-module-scripts 56 made `prepare` a no-op and `prepublishOnly` only rebuilds the root build/, so npm publish shipped whatever plugin/build was on disk. 0.9.2 went out with 0.9.1's plugin output and no Liquid Glass icon support. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 ++ package.json | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ce79d8..c16fa7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,3 +17,5 @@ jobs: run: bun install - name: Build react-native-app-clip run: bun run build + - name: Build config plugin + run: bun run build:plugin diff --git a/package.json b/package.json index 2b2c858..4b151eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-app-clip", - "version": "0.9.2", + "version": "0.9.3", "description": "Config plugin to add an App Clip to a React Native iOS app", "main": "build/index.js", "types": "build/index.d.ts", @@ -19,7 +19,7 @@ "lint": "expo-module lint", "test": "expo-module test", "prepare": "expo-module prepare", - "prepublishOnly": "expo-module prepublishOnly", + "prepublishOnly": "expo-module prepublishOnly && expo-module clean plugin && tsc --build plugin", "expo-module": "expo-module", "open:ios": "xed example/ios", "open:android": "open -a \"Android Studio\" example/android" From 8a40bad05b294f0b18edcbb95f7158dcf01ff764 Mon Sep 17 00:00:00 2001 From: Kyrylo Orlov Date: Tue, 20 Oct 2026 12:36:21 -0400 Subject: [PATCH 2/4] Revert CI and package.json changes Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 -- package.json | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c16fa7b..3ce79d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,5 +17,3 @@ jobs: run: bun install - name: Build react-native-app-clip run: bun run build - - name: Build config plugin - run: bun run build:plugin diff --git a/package.json b/package.json index 4b151eb..2b2c858 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-app-clip", - "version": "0.9.3", + "version": "0.9.2", "description": "Config plugin to add an App Clip to a React Native iOS app", "main": "build/index.js", "types": "build/index.d.ts", @@ -19,7 +19,7 @@ "lint": "expo-module lint", "test": "expo-module test", "prepare": "expo-module prepare", - "prepublishOnly": "expo-module prepublishOnly && expo-module clean plugin && tsc --build plugin", + "prepublishOnly": "expo-module prepublishOnly", "expo-module": "expo-module", "open:ios": "xed example/ios", "open:android": "open -a \"Android Studio\" example/android" From 09d885f7b41d63400b55b88beabcb413a45c5252 Mon Sep 17 00:00:00 2001 From: Kyrylo Orlov Date: Tue, 20 Oct 2026 12:36:54 -0400 Subject: [PATCH 3/4] Add compiled config plugin build plugin/build is gitignored, so npm publish packed whatever was on the publisher's disk and 0.9.2 shipped 0.9.1's plugin output without the Liquid Glass icon changes. Committing the build keeps the published plugin in sync with plugin/src. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- plugin/build/cliPlugin.d.ts | 3 + plugin/build/cliPlugin.js | 26 ++++ plugin/build/index.d.ts | 17 +++ plugin/build/index.js | 61 ++++++++++ plugin/build/lib/getAppClipEntitlements.d.ts | 8 ++ plugin/build/lib/getAppClipEntitlements.js | 39 ++++++ plugin/build/lib/getAppClipIcon.d.ts | 5 + plugin/build/lib/getAppClipIcon.js | 20 ++++ plugin/build/withConfig.d.ts | 8 ++ plugin/build/withConfig.js | 66 +++++++++++ plugin/build/withEntitlements.d.ts | 8 ++ plugin/build/withEntitlements.js | 30 +++++ plugin/build/withPlist.d.ts | 7 ++ plugin/build/withPlist.js | 79 ++++++++++++ plugin/build/withPodfile.d.ts | 5 + plugin/build/withPodfile.js | 77 ++++++++++++ plugin/build/withXcode.d.ts | 8 ++ plugin/build/withXcode.js | 81 +++++++++++++ plugin/build/xcode/addBuildPhases.d.ts | 13 ++ plugin/build/xcode/addBuildPhases.js | 44 +++++++ plugin/build/xcode/addPbxGroup.d.ts | 7 ++ plugin/build/xcode/addPbxGroup.js | 72 +++++++++++ plugin/build/xcode/addProductFile.d.ts | 5 + plugin/build/xcode/addProductFile.js | 21 ++++ plugin/build/xcode/addTargetDependency.d.ts | 4 + plugin/build/xcode/addTargetDependency.js | 14 +++ .../xcode/addToPbxNativeTargetSection.d.ts | 24 ++++ .../xcode/addToPbxNativeTargetSection.js | 21 ++++ .../build/xcode/addToPbxProjectSection.d.ts | 4 + plugin/build/xcode/addToPbxProjectSection.js | 14 +++ .../build/xcode/addXCConfigurationList.d.ts | 10 ++ plugin/build/xcode/addXCConfigurationList.js | 41 +++++++ plugin/build/xcode/modifyAppDelegate.d.ts | 1 + plugin/build/xcode/modifyAppDelegate.js | 112 ++++++++++++++++++ 34 files changed, 955 insertions(+) create mode 100644 plugin/build/cliPlugin.d.ts create mode 100644 plugin/build/cliPlugin.js create mode 100644 plugin/build/index.d.ts create mode 100644 plugin/build/index.js create mode 100644 plugin/build/lib/getAppClipEntitlements.d.ts create mode 100644 plugin/build/lib/getAppClipEntitlements.js create mode 100644 plugin/build/lib/getAppClipIcon.d.ts create mode 100644 plugin/build/lib/getAppClipIcon.js create mode 100644 plugin/build/withConfig.d.ts create mode 100644 plugin/build/withConfig.js create mode 100644 plugin/build/withEntitlements.d.ts create mode 100644 plugin/build/withEntitlements.js create mode 100644 plugin/build/withPlist.d.ts create mode 100644 plugin/build/withPlist.js create mode 100644 plugin/build/withPodfile.d.ts create mode 100644 plugin/build/withPodfile.js create mode 100644 plugin/build/withXcode.d.ts create mode 100644 plugin/build/withXcode.js create mode 100644 plugin/build/xcode/addBuildPhases.d.ts create mode 100644 plugin/build/xcode/addBuildPhases.js create mode 100644 plugin/build/xcode/addPbxGroup.d.ts create mode 100644 plugin/build/xcode/addPbxGroup.js create mode 100644 plugin/build/xcode/addProductFile.d.ts create mode 100644 plugin/build/xcode/addProductFile.js create mode 100644 plugin/build/xcode/addTargetDependency.d.ts create mode 100644 plugin/build/xcode/addTargetDependency.js create mode 100644 plugin/build/xcode/addToPbxNativeTargetSection.d.ts create mode 100644 plugin/build/xcode/addToPbxNativeTargetSection.js create mode 100644 plugin/build/xcode/addToPbxProjectSection.d.ts create mode 100644 plugin/build/xcode/addToPbxProjectSection.js create mode 100644 plugin/build/xcode/addXCConfigurationList.d.ts create mode 100644 plugin/build/xcode/addXCConfigurationList.js create mode 100644 plugin/build/xcode/modifyAppDelegate.d.ts create mode 100644 plugin/build/xcode/modifyAppDelegate.js diff --git a/plugin/build/cliPlugin.d.ts b/plugin/build/cliPlugin.d.ts new file mode 100644 index 0000000..ff50448 --- /dev/null +++ b/plugin/build/cliPlugin.d.ts @@ -0,0 +1,3 @@ +export declare const run: (config: { + dependencies: Record; +}, exclude: string[]) => Promise; diff --git a/plugin/build/cliPlugin.js b/plugin/build/cliPlugin.js new file mode 100644 index 0000000..0e0fcc5 --- /dev/null +++ b/plugin/build/cliPlugin.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.run = void 0; +const run = async (config, exclude) => { + // Validation + if (typeof config !== "object") { + throw new Error("config must be an object"); + } + if (!config.dependencies) { + throw new Error("config must have a dependencies key"); + } + if (!Array.isArray(exclude)) { + throw new Error("exclude must be an array"); + } + for (const packageName of exclude) { + if (typeof packageName !== "string") { + throw new Error("exclude must be an array of strings"); + } + } + // Removing excluded packages + for (const packageName of exclude) { + delete config.dependencies[packageName]; + } + console.log(JSON.stringify(config)); +}; +exports.run = run; diff --git a/plugin/build/index.d.ts b/plugin/build/index.d.ts new file mode 100644 index 0000000..e93cf17 --- /dev/null +++ b/plugin/build/index.d.ts @@ -0,0 +1,17 @@ +import { type ConfigPlugin } from "expo/config-plugins"; +declare const withAppClip: ConfigPlugin<{ + name?: string; + enabled?: boolean; + bundleIdSuffix?: string; + targetSuffix?: string; + groupIdentifier?: string; + deploymentTarget?: string; + requestEphemeralUserNotification?: boolean; + requestLocationConfirmation?: boolean; + appleSignin?: boolean; + applePayMerchantIds?: string[]; + excludedPackages?: string[]; + pushNotifications?: boolean; + enableCompression?: boolean; +}>; +export default withAppClip; diff --git a/plugin/build/index.js b/plugin/build/index.js new file mode 100644 index 0000000..32e9fb4 --- /dev/null +++ b/plugin/build/index.js @@ -0,0 +1,61 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const config_plugins_1 = require("expo/config-plugins"); +const withConfig_1 = require("./withConfig"); +const withEntitlements_1 = require("./withEntitlements"); +const withPlist_1 = require("./withPlist"); +const withPodfile_1 = require("./withPodfile"); +const withXcode_1 = require("./withXcode"); +const DeviceFamily_1 = require("@expo/config-plugins/build/ios/DeviceFamily"); +const withAppClip = (config, { name, enabled, bundleIdSuffix, targetSuffix, groupIdentifier, deploymentTarget, requestEphemeralUserNotification, requestLocationConfirmation, appleSignin, applePayMerchantIds, excludedPackages, pushNotifications, enableCompression, } = {}) => { + name ??= "Clip"; + bundleIdSuffix ??= "Clip"; + targetSuffix ??= "Clip"; + deploymentTarget ??= "15.1"; + requestEphemeralUserNotification ??= false; + requestLocationConfirmation ??= false; + appleSignin ??= false; + enabled ??= true; + pushNotifications ??= false; + if (!enabled) { + return config; + } + if (!config.ios?.bundleIdentifier) { + throw new Error("No bundle identifier specified in app config"); + } + const bundleIdentifier = `${config.ios.bundleIdentifier}.${bundleIdSuffix}`; + const targetName = `${config_plugins_1.IOSConfig.XcodeUtils.sanitizedName(config.name)}${targetSuffix}`; + const modifiedConfig = (0, config_plugins_1.withPlugins)(config, [ + DeviceFamily_1.withDeviceFamily, + [ + withConfig_1.withConfig, + { targetName, bundleIdentifier, appleSignin, applePayMerchantIds, pushNotifications }, + ], + [ + withEntitlements_1.withEntitlements, + { targetName, groupIdentifier, appleSignin, applePayMerchantIds, pushNotifications }, + ], + [withPodfile_1.withPodfile, { targetName, excludedPackages }], + [ + withPlist_1.withPlist, + { + targetName, + deploymentTarget, + requestEphemeralUserNotification, + requestLocationConfirmation, + }, + ], + [ + withXcode_1.withXcode, + { + name, + targetName, + bundleIdentifier, + deploymentTarget, + enableCompression, + }, + ], + ]); + return modifiedConfig; +}; +exports.default = withAppClip; diff --git a/plugin/build/lib/getAppClipEntitlements.d.ts b/plugin/build/lib/getAppClipEntitlements.d.ts new file mode 100644 index 0000000..ada8dfb --- /dev/null +++ b/plugin/build/lib/getAppClipEntitlements.d.ts @@ -0,0 +1,8 @@ +import type { ExportedConfigWithProps, InfoPlist } from "expo/config-plugins"; +export declare function getAppClipEntitlements(iosConfig: ExportedConfigWithProps["ios"], { groupIdentifier, appleSignin, applePayMerchantIds, pushNotifications, }: { + groupIdentifier?: string; + appleSignin: boolean; + applePayMerchantIds?: string[]; + pushNotifications: boolean; +}): InfoPlist; +export declare function addApplicationGroupsEntitlement(entitlements: InfoPlist, groupIdentifier?: string): InfoPlist; diff --git a/plugin/build/lib/getAppClipEntitlements.js b/plugin/build/lib/getAppClipEntitlements.js new file mode 100644 index 0000000..4aa79e1 --- /dev/null +++ b/plugin/build/lib/getAppClipEntitlements.js @@ -0,0 +1,39 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAppClipEntitlements = getAppClipEntitlements; +exports.addApplicationGroupsEntitlement = addApplicationGroupsEntitlement; +function getAppClipEntitlements(iosConfig, { groupIdentifier, appleSignin, applePayMerchantIds, pushNotifications, }) { + const appBundleIdentifier = iosConfig?.bundleIdentifier; + const entitlements = { + "com.apple.developer.parent-application-identifiers": [ + `$(AppIdentifierPrefix)${appBundleIdentifier}`, + ], + "com.apple.developer.on-demand-install-capable": true, + }; + addApplicationGroupsEntitlement(entitlements, groupIdentifier); + if (appleSignin) { + entitlements["com.apple.developer.applesignin"] = ["Default"]; + } + if (applePayMerchantIds) { + entitlements["com.apple.developer.in-app-payments"] = applePayMerchantIds; + } + if (pushNotifications) { + // XCode automatically changes this to "production" when archiving https://stackoverflow.com/a/42293632/4047926 + entitlements["aps-environment"] = "development"; + } + if (iosConfig?.associatedDomains) { + entitlements["com.apple.developer.associated-domains"] = + iosConfig.associatedDomains; + } + return entitlements; +} +function addApplicationGroupsEntitlement(entitlements, groupIdentifier) { + if (groupIdentifier) { + const existingApplicationGroups = entitlements["com.apple.security.application-groups"] ?? []; + entitlements["com.apple.security.application-groups"] = [ + groupIdentifier, + ...existingApplicationGroups, + ]; + } + return entitlements; +} diff --git a/plugin/build/lib/getAppClipIcon.d.ts b/plugin/build/lib/getAppClipIcon.d.ts new file mode 100644 index 0000000..cef2122 --- /dev/null +++ b/plugin/build/lib/getAppClipIcon.d.ts @@ -0,0 +1,5 @@ +import type { ConfigPlugin } from "expo/config-plugins"; +export declare function getAppClipIcon(config: Pick[0], "icon" | "ios">): { + name: string; + fileName: string; +} | undefined; diff --git a/plugin/build/lib/getAppClipIcon.js b/plugin/build/lib/getAppClipIcon.js new file mode 100644 index 0000000..511cd32 --- /dev/null +++ b/plugin/build/lib/getAppClipIcon.js @@ -0,0 +1,20 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAppClipIcon = getAppClipIcon; +const node_path_1 = __importDefault(require("node:path")); +function getAppClipIcon(config) { + let icon = config.ios?.icon || config.icon; + if (icon && typeof icon === "object" && !icon.light && !icon.dark && !icon.tinted) { + icon = config.icon; + } + if (typeof icon !== "string" || node_path_1.default.extname(icon) !== ".icon") { + return undefined; + } + return { + name: node_path_1.default.basename(icon, ".icon"), + fileName: node_path_1.default.basename(icon), + }; +} diff --git a/plugin/build/withConfig.d.ts b/plugin/build/withConfig.d.ts new file mode 100644 index 0000000..6f3644c --- /dev/null +++ b/plugin/build/withConfig.d.ts @@ -0,0 +1,8 @@ +import type { ConfigPlugin } from "expo/config-plugins"; +export declare const withConfig: ConfigPlugin<{ + targetName: string; + bundleIdentifier: string; + appleSignin: boolean; + applePayMerchantIds: string[]; + pushNotifications: boolean; +}>; diff --git a/plugin/build/withConfig.js b/plugin/build/withConfig.js new file mode 100644 index 0000000..4d0013c --- /dev/null +++ b/plugin/build/withConfig.js @@ -0,0 +1,66 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withConfig = void 0; +const getAppClipEntitlements_1 = require("./lib/getAppClipEntitlements"); +const withConfig = (config, { targetName, bundleIdentifier, appleSignin, applePayMerchantIds, pushNotifications }) => { + let configIndex = null; + config.extra?.eas?.build?.experimental?.ios?.appExtensions?.forEach((ext, index) => { + if (ext.targetName === targetName) { + configIndex = index; + } + }); + if (!configIndex) { + config.extra = { + ...config.extra, + eas: { + ...config.extra?.eas, + build: { + ...config.extra?.eas?.build, + experimental: { + ...config.extra?.eas?.build?.experimental, + ios: { + ...config.extra?.eas?.build?.experimental?.ios, + appExtensions: [ + ...(config.extra?.eas?.build?.experimental?.ios + ?.appExtensions ?? []), + { + targetName, + bundleIdentifier, + }, + ], + }, + }, + }, + }, + }; + configIndex = 0; + } + if (configIndex !== null && config.extra) { + const appClipConfig = config.extra.eas.build.experimental.ios.appExtensions[configIndex]; + appClipConfig.entitlements = { + ...appClipConfig.entitlements, + ...(0, getAppClipEntitlements_1.getAppClipEntitlements)(config.ios, { + appleSignin, + applePayMerchantIds, + // groupIdentifier, // Throws an error in EAS + pushNotifications, + }), + }; + } + // Entitlements + config.ios = { + ...config.ios, + entitlements: { + // ...addApplicationGroupsEntitlement( + // config.ios?.entitlements ?? {}, + // groupIdentifier, + // ), + ...config.ios?.entitlements, + "com.apple.developer.associated-appclip-app-identifiers": [ + `$(AppIdentifierPrefix)${bundleIdentifier}`, + ], + }, + }; + return config; +}; +exports.withConfig = withConfig; diff --git a/plugin/build/withEntitlements.d.ts b/plugin/build/withEntitlements.d.ts new file mode 100644 index 0000000..dedc537 --- /dev/null +++ b/plugin/build/withEntitlements.d.ts @@ -0,0 +1,8 @@ +import { type ConfigPlugin } from "expo/config-plugins"; +export declare const withEntitlements: ConfigPlugin<{ + targetName: string; + groupIdentifier: string; + appleSignin: boolean; + applePayMerchantIds: string[]; + pushNotifications: boolean; +}>; diff --git a/plugin/build/withEntitlements.js b/plugin/build/withEntitlements.js new file mode 100644 index 0000000..d4051dc --- /dev/null +++ b/plugin/build/withEntitlements.js @@ -0,0 +1,30 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withEntitlements = void 0; +const plist_1 = __importDefault(require("@expo/plist")); +const config_plugins_1 = require("expo/config-plugins"); +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const getAppClipEntitlements_1 = require("./lib/getAppClipEntitlements"); +const withEntitlements = (config, { targetName, groupIdentifier, appleSignin, applePayMerchantIds, pushNotifications }) => { + return (0, config_plugins_1.withInfoPlist)(config, (config) => { + const targetPath = node_path_1.default.join(config.modRequest.platformProjectRoot, targetName); + const filePath = node_path_1.default.join(targetPath, `${targetName}.entitlements`); + if (config.ios === undefined) { + throw new Error("Missing iOS config"); + } + const appClipEntitlements = (0, getAppClipEntitlements_1.getAppClipEntitlements)(config.ios, { + groupIdentifier, + appleSignin, + applePayMerchantIds, + pushNotifications, + }); + node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true }); + node_fs_1.default.writeFileSync(filePath, plist_1.default.build(appClipEntitlements)); + return config; + }); +}; +exports.withEntitlements = withEntitlements; diff --git a/plugin/build/withPlist.d.ts b/plugin/build/withPlist.d.ts new file mode 100644 index 0000000..8a17c46 --- /dev/null +++ b/plugin/build/withPlist.d.ts @@ -0,0 +1,7 @@ +import { type ConfigPlugin } from "expo/config-plugins"; +export declare const withPlist: ConfigPlugin<{ + targetName: string; + deploymentTarget: string; + requestEphemeralUserNotification: boolean; + requestLocationConfirmation: boolean; +}>; diff --git a/plugin/build/withPlist.js b/plugin/build/withPlist.js new file mode 100644 index 0000000..525562c --- /dev/null +++ b/plugin/build/withPlist.js @@ -0,0 +1,79 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withPlist = void 0; +const plist_1 = __importDefault(require("@expo/plist")); +const config_plugins_1 = require("expo/config-plugins"); +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const getAppClipIcon_1 = require("./lib/getAppClipIcon"); +/** + * List of keys that are not allowed in the Info.plist due to not being supported in App Clip. Add to this array as new build issues arise. + */ +const DisallowedPlistKeys = [ + "UIBackgroundModes", +]; +const withPlist = (config, { targetName, deploymentTarget, requestEphemeralUserNotification, requestLocationConfirmation, }) => { + return (0, config_plugins_1.withInfoPlist)(config, (config) => { + const targetPath = node_path_1.default.join(config.modRequest.platformProjectRoot, targetName); + // Info.plist + const filePath = node_path_1.default.join(targetPath, "Info.plist"); + const infoPlist = { + NSAppClip: { + NSAppClipRequestEphemeralUserNotification: requestEphemeralUserNotification, + NSAppClipRequestLocationConfirmation: requestLocationConfirmation, + }, + NSAppTransportSecurity: { + NSAllowsArbitraryLoads: config.developmentClient, + NSExceptionDomains: { + localhost: { + NSExceptionAllowsInsecureHTTPLoads: config.developmentClient, + }, + }, + NSAllowsLocalNetworking: config.developmentClient, + }, + CFBundleName: "$(PRODUCT_MODULE_NAME)", + CFBundleDisplayName: "$(PRODUCT_NAME)", + CFBundleIdentifier: "$(PRODUCT_BUNDLE_IDENTIFIER)", + CFBundleVersion: "$(CURRENT_PROJECT_VERSION)", + CFBundleExecutable: "$(EXECUTABLE_NAME)", + CFBundlePackageType: "$(PRODUCT_BUNDLE_PACKAGE_TYPE)", + CFBundleShortVersionString: config.version, + UIViewControllerBasedStatusBarAppearance: "NO", + UILaunchStoryboardName: "SplashScreen", + UIRequiresFullScreen: true, + MinimumOSVersion: deploymentTarget, + }; + if (config.ios?.infoPlist) { + for (const key of Object.keys(config.ios?.infoPlist)) { + if (config.ios?.infoPlist && !DisallowedPlistKeys.some((disallowedKey) => disallowedKey === key)) { + infoPlist[key] = config.ios.infoPlist[key]; + } + } + } + infoPlist.CFBundleName = "$(PRODUCT_MODULE_NAME)"; + const appClipIcon = (0, getAppClipIcon_1.getAppClipIcon)(config); + if (appClipIcon) { + infoPlist.CFBundleIconName = appClipIcon.name; + } + node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { + recursive: true, + }); + node_fs_1.default.writeFileSync(filePath, plist_1.default.build(infoPlist)); + // Expo.plist + const expoPlistFilePath = node_path_1.default.join(targetPath, "Supporting/Expo.plist"); + const expoPlist = { + EXUpdatesRuntimeVersion: "exposdk:51.0.0", // TODO + // EXUpdatesURL: "", // TODO + EXUpdatesEnabled: false, + }; + node_fs_1.default.mkdirSync(node_path_1.default.dirname(expoPlistFilePath), { + recursive: true, + }); + node_fs_1.default.writeFileSync(expoPlistFilePath, plist_1.default.build(expoPlist)); + return config; + }); +}; +exports.withPlist = withPlist; diff --git a/plugin/build/withPodfile.d.ts b/plugin/build/withPodfile.d.ts new file mode 100644 index 0000000..b818fdd --- /dev/null +++ b/plugin/build/withPodfile.d.ts @@ -0,0 +1,5 @@ +import { type ConfigPlugin } from "expo/config-plugins"; +export declare const withPodfile: ConfigPlugin<{ + targetName: string; + excludedPackages?: string[]; +}>; diff --git a/plugin/build/withPodfile.js b/plugin/build/withPodfile.js new file mode 100644 index 0000000..1b10c9e --- /dev/null +++ b/plugin/build/withPodfile.js @@ -0,0 +1,77 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withPodfile = void 0; +const config_plugins_1 = require("expo/config-plugins"); +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const withPodfile = (config, { targetName, excludedPackages }) => { + return (0, config_plugins_1.withDangerousMod)(config, [ + "ios", + (config) => { + const podFilePath = node_path_1.default.join(config.modRequest.platformProjectRoot, "Podfile"); + let podfileContent = node_fs_1.default.readFileSync(podFilePath).toString(); + const useExpoModules = excludedPackages && excludedPackages.length > 0 + ? `exclude = ["${excludedPackages.join(`", "`)}"]\n use_expo_modules!(exclude: exclude)` + : "use_expo_modules!"; + const appClipTarget = ` +# @generated begin react-native-app-clip +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 + + # 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 + + # \`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 + ', [])' + ] + + config = use_native_modules!(clip_command) + + 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'] + + 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`; + node_fs_1.default.writeFileSync(podFilePath, podfileContent); + return config; + }, + ]); +}; +exports.withPodfile = withPodfile; diff --git a/plugin/build/withXcode.d.ts b/plugin/build/withXcode.d.ts new file mode 100644 index 0000000..e7ff1f2 --- /dev/null +++ b/plugin/build/withXcode.d.ts @@ -0,0 +1,8 @@ +import { type ConfigPlugin } from "expo/config-plugins"; +export declare const withXcode: ConfigPlugin<{ + name: string; + targetName: string; + bundleIdentifier: string; + deploymentTarget: string; + enableCompression?: boolean; +}>; diff --git a/plugin/build/withXcode.js b/plugin/build/withXcode.js new file mode 100644 index 0000000..83dd7ef --- /dev/null +++ b/plugin/build/withXcode.js @@ -0,0 +1,81 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withXcode = void 0; +const config_plugins_1 = require("expo/config-plugins"); +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const getAppClipIcon_1 = require("./lib/getAppClipIcon"); +const addBuildPhases_1 = require("./xcode/addBuildPhases"); +const addPbxGroup_1 = require("./xcode/addPbxGroup"); +const modifyAppDelegate_1 = require("./xcode/modifyAppDelegate"); +const addProductFile_1 = require("./xcode/addProductFile"); +const addTargetDependency_1 = require("./xcode/addTargetDependency"); +const addToPbxNativeTargetSection_1 = require("./xcode/addToPbxNativeTargetSection"); +const addToPbxProjectSection_1 = require("./xcode/addToPbxProjectSection"); +const addXCConfigurationList_1 = require("./xcode/addXCConfigurationList"); +const withXcode = (config, { name, targetName, bundleIdentifier, deploymentTarget, enableCompression }) => { + return (0, config_plugins_1.withXcodeProject)(config, (config) => { + const xcodeProject = config.modResults; + const appClipIcon = (0, getAppClipIcon_1.getAppClipIcon)(config); + const targetUuid = xcodeProject.generateUuid(); + const groupName = "Embed App Clips"; + const { projectName, platformProjectRoot } = config.modRequest; + const xCConfigurationList = (0, addXCConfigurationList_1.addXCConfigurationList)(xcodeProject, { + name, + targetName, + currentProjectVersion: config.ios?.buildNumber || "1", + bundleIdentifier, + deploymentTarget, + appleTeamId: config.ios?.appleTeamId, + appIconName: appClipIcon?.name, + }); + const productFile = (0, addProductFile_1.addProductFile)(xcodeProject, { + targetName, + groupName, + }); + const target = (0, addToPbxNativeTargetSection_1.addToPbxNativeTargetSection)(xcodeProject, { + targetName, + targetUuid, + productFile, + xCConfigurationList, + }); + (0, addToPbxProjectSection_1.addToPbxProjectSection)(xcodeProject, target); + (0, addTargetDependency_1.addTargetDependency)(xcodeProject, target); + (0, addPbxGroup_1.addPbxGroup)(xcodeProject, { + projectName: projectName, + targetName, + platformProjectRoot, + appIconFileName: appClipIcon?.fileName, + }); + if (enableCompression) { + const appDelegatePath = node_path_1.default.join(platformProjectRoot, targetName, "AppDelegate.swift"); + if (node_fs_1.default.existsSync(appDelegatePath)) { + (0, modifyAppDelegate_1.modifyAppDelegate)(appDelegatePath); + } + else { + console.warn(`⚠️ AppDelegate not found at ${appDelegatePath}`); + } + } + (0, addBuildPhases_1.addBuildPhases)(xcodeProject, { + targetUuid, + groupName, + productFile, + enableCompression, + resourcesPhaseName: appClipIcon ? "Resources" : undefined, + }); + if (appClipIcon) { + config_plugins_1.IOSConfig.XcodeUtils.addResourceFileToGroup({ + filepath: appClipIcon.fileName, + groupName: targetName, + project: xcodeProject, + isBuildFile: true, + targetUuid, + }); + } + return config; + }); +}; +exports.withXcode = withXcode; diff --git a/plugin/build/xcode/addBuildPhases.d.ts b/plugin/build/xcode/addBuildPhases.d.ts new file mode 100644 index 0000000..38d3b42 --- /dev/null +++ b/plugin/build/xcode/addBuildPhases.d.ts @@ -0,0 +1,13 @@ +import type { XcodeProject } from "expo/config-plugins"; +export declare function addBuildPhases(xcodeProject: XcodeProject, { targetUuid, groupName, productFile, enableCompression, resourcesPhaseName, }: { + targetUuid: string; + groupName: string; + productFile: { + uuid: string; + target: string; + basename: string; + group: string; + }; + enableCompression?: boolean; + resourcesPhaseName?: string; +}): void; diff --git a/plugin/build/xcode/addBuildPhases.js b/plugin/build/xcode/addBuildPhases.js new file mode 100644 index 0000000..bf9c491 --- /dev/null +++ b/plugin/build/xcode/addBuildPhases.js @@ -0,0 +1,44 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addBuildPhases = addBuildPhases; +const node_util_1 = __importDefault(require("node:util")); +function addBuildPhases(xcodeProject, { targetUuid, groupName, productFile, enableCompression, resourcesPhaseName, }) { + const buildPath = `"$(CONTENTS_FOLDER_PATH)/AppClips"`; + const folderType = "watch2_app"; // "watch2_app" uses the same subfolder spec (16), app_clip does not exist in cordova-node-xcode yet + const buildPhaseFiles = ["AppDelegate.swift"]; + // Add shell script build phase "Start Packager" + xcodeProject.addBuildPhase([], "PBXShellScriptBuildPhase", "Start Packager", targetUuid, { + shellPath: "/bin/sh", + shellScript: 'if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then\n source "$PODS_ROOT/../.xcode.env"\nfi\nif [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then\n source "$PODS_ROOT/../.xcode.env.local"\nfi\n\nexport RCT_METRO_PORT="${RCT_METRO_PORT:=8081}"\necho "export RCT_METRO_PORT=${RCT_METRO_PORT}" > `$NODE_BINARY --print "require(\'path\').dirname(require.resolve(\'react-native/package.json\')) + \'/scripts/.packager.env\'"`\nif [ -z "${RCT_NO_LAUNCH_PACKAGER+xxx}" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s "http://localhost:${RCT_METRO_PORT}/status" | grep -q "packager-status:running" ; then\n echo "Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly"\n exit 2\n fi\n else\n open `$NODE_BINARY --print "require(\'path\').dirname(require.resolve(\'expo/package.json\')) + \'/scripts/launchPackager.command\'"` || echo "Can\'t start packager automatically"\n fi\nfi\n', + }, buildPath); + // Sources build phase + xcodeProject.addBuildPhase(buildPhaseFiles, "PBXSourcesBuildPhase", groupName, targetUuid, folderType, buildPath); + // Copy files build phase + xcodeProject.addBuildPhase([], "PBXCopyFilesBuildPhase", groupName, xcodeProject.getFirstTarget().uuid, folderType, buildPath); + xcodeProject + .buildPhaseObject("PBXCopyFilesBuildPhase", groupName, productFile.target) + .files.push({ + value: productFile.uuid, + comment: node_util_1.default.format("%s in %s", productFile.basename, productFile.group), // longComment(file); + }); + xcodeProject.addToPbxBuildFileSection(productFile); + // Frameworks build phase + xcodeProject.addBuildPhase([], "PBXFrameworksBuildPhase", groupName, targetUuid, folderType, buildPath); + // Resources build phase + xcodeProject.addBuildPhase(["Images.xcassets", "SplashScreen.storyboard", "Supporting/Expo.plist"], "PBXResourcesBuildPhase", resourcesPhaseName ?? groupName, targetUuid, folderType, buildPath); + // Add shell script build phase + xcodeProject.addBuildPhase([], "PBXShellScriptBuildPhase", "Bundle React Native code and images", targetUuid, { + shellPath: "/bin/sh", + shellScript: 'export BUILDING_FOR_APP_CLIP=1\nif [[ -f "$PODS_ROOT/../.xcode.env" ]]; then\n source "$PODS_ROOT/../.xcode.env"\nfi\nif [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then\n source "$PODS_ROOT/../.xcode.env.local"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT="$PROJECT_DIR"/..\n\nif [[ -f "$PROJECT_ROOT/.env" ]]; then\n set -a\n source "$PROJECT_ROOT/.env"\n set +a\nfi\n\nif [[ "$CONFIGURATION" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z "$ENTRY_FILE" ]]; then\n # Set the entry JS file using the bundler\'s entry resolution.\n export ENTRY_FILE="$("$NODE_BINARY" -e "require(\'expo/scripts/resolveAppEntry\')" "$PROJECT_ROOT" ios relative | tail -n 1)"\nfi\n\nif [[ -z "$CLI_PATH" ]]; then\n # Use Expo CLI\n export CLI_PATH="$("$NODE_BINARY" --print "require.resolve(\'@expo/cli\')")"\nfi\nif [[ -z "$BUNDLE_COMMAND" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND="export:embed"\nfi\n\n`"$NODE_BINARY" --print "require(\'path\').dirname(require.resolve(\'react-native/package.json\')) + \'/scripts/react-native-xcode.sh\'"`\n\n', + }, buildPath); + // Add compression build phase if enabled + if (enableCompression) { + xcodeProject.addBuildPhase([], "PBXShellScriptBuildPhase", "Compress JS Bundle", targetUuid, { + shellPath: "/bin/sh", + shellScript: 'if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then\n source "$PODS_ROOT/../.xcode.env"\nfi\nif [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then\n source "$PODS_ROOT/../.xcode.env.local"\nfi\n\nif [[ "$CONFIGURATION" != "Release" ]]; then\n exit 0\nfi\n\nBUNDLE="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/main.jsbundle"\n\nif [ -f "$BUNDLE" ]; then\n "$NODE_BINARY" -e "const fs=require(\'fs\');const zlib=require(\'zlib\');const data=fs.readFileSync(\'${BUNDLE}\');const compressed=zlib.gzipSync(data,{level:9});fs.writeFileSync(\'${BUNDLE}.gz\',compressed);fs.unlinkSync(\'${BUNDLE}\');"\nfi\n', + }, buildPath); + } +} diff --git a/plugin/build/xcode/addPbxGroup.d.ts b/plugin/build/xcode/addPbxGroup.d.ts new file mode 100644 index 0000000..e539cec --- /dev/null +++ b/plugin/build/xcode/addPbxGroup.d.ts @@ -0,0 +1,7 @@ +import type { XcodeProject } from "expo/config-plugins"; +export declare function addPbxGroup(xcodeProject: XcodeProject, { projectName, targetName, platformProjectRoot, appIconFileName, }: { + projectName: string; + targetName: string; + platformProjectRoot: string; + appIconFileName?: string; +}): void; diff --git a/plugin/build/xcode/addPbxGroup.js b/plugin/build/xcode/addPbxGroup.js new file mode 100644 index 0000000..0c06652 --- /dev/null +++ b/plugin/build/xcode/addPbxGroup.js @@ -0,0 +1,72 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addPbxGroup = addPbxGroup; +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +function addPbxGroup(xcodeProject, { projectName, targetName, platformProjectRoot, appIconFileName, }) { + const targetPath = node_path_1.default.join(platformProjectRoot, targetName); + if (!node_fs_1.default.existsSync(targetPath)) { + node_fs_1.default.mkdirSync(targetPath, { recursive: true }); + } + const filesToCopy = [ + "SplashScreen.storyboard", + "AppDelegate.swift", + ]; + for (const file of filesToCopy) { + const source = node_path_1.default.join(platformProjectRoot, projectName, file); + copyFileSync(source, targetPath); + } + // Copy Images.xcassets + const imagesXcassetsSource = node_path_1.default.join(platformProjectRoot, projectName, "Images.xcassets"); + copyFolderRecursiveSync(imagesXcassetsSource, targetPath); + if (appIconFileName) { + node_fs_1.default.cpSync(node_path_1.default.join(platformProjectRoot, projectName, appIconFileName), node_path_1.default.join(targetPath, appIconFileName), { recursive: true }); + } + // Add PBX group + const { uuid: pbxGroupUuid } = xcodeProject.addPbxGroup([ + "AppDelegate.swift", + "Info.plist", + "Images.xcassets", + "SplashScreen.storyboard", + `${targetName}.entitlements`, + "Supporting/Expo.plist", + /* "main.jsbundle", */ + ], targetName, targetName); + // Add PBXGroup to top level group + const groups = xcodeProject.hash.project.objects.PBXGroup; + if (pbxGroupUuid) { + for (const key of Object.keys(groups)) { + if (groups[key].name === undefined && groups[key].path === undefined) { + xcodeProject.addToPbxGroup(pbxGroupUuid, key); + } + } + } +} +function copyFileSync(source, target) { + let targetFile = target; + if (node_fs_1.default.existsSync(target) && node_fs_1.default.lstatSync(target).isDirectory()) { + targetFile = node_path_1.default.join(target, node_path_1.default.basename(source)); + } + node_fs_1.default.writeFileSync(targetFile, node_fs_1.default.readFileSync(source)); +} +function copyFolderRecursiveSync(source, target) { + const targetPath = node_path_1.default.join(target, node_path_1.default.basename(source)); + if (!node_fs_1.default.existsSync(targetPath)) { + node_fs_1.default.mkdirSync(targetPath, { recursive: true }); + } + if (node_fs_1.default.lstatSync(source).isDirectory()) { + const files = node_fs_1.default.readdirSync(source); + for (const file of files) { + const currentPath = node_path_1.default.join(source, file); + if (node_fs_1.default.lstatSync(currentPath).isDirectory()) { + copyFolderRecursiveSync(currentPath, targetPath); + } + else { + copyFileSync(currentPath, targetPath); + } + } + } +} diff --git a/plugin/build/xcode/addProductFile.d.ts b/plugin/build/xcode/addProductFile.d.ts new file mode 100644 index 0000000..66fdb5f --- /dev/null +++ b/plugin/build/xcode/addProductFile.d.ts @@ -0,0 +1,5 @@ +import type { XcodeProject } from "expo/config-plugins"; +export declare function addProductFile(xcodeProject: XcodeProject, { targetName, groupName }: { + targetName: string; + groupName: string; +}): any; diff --git a/plugin/build/xcode/addProductFile.js b/plugin/build/xcode/addProductFile.js new file mode 100644 index 0000000..2dc3217 --- /dev/null +++ b/plugin/build/xcode/addProductFile.js @@ -0,0 +1,21 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addProductFile = addProductFile; +function addProductFile(xcodeProject, { targetName, groupName }) { + const options = { + basename: `${targetName}.app`, + // fileRef: xcodeProject.generateUuid(), + // uuid: xcodeProject.generateUuid(), + group: groupName, + explicitFileType: "wrapper.application", + /* fileEncoding: 4, */ + settings: { + ATTRIBUTES: ["RemoveHeadersOnCopy"], + }, + includeInIndex: 0, + path: `${targetName}.app`, + sourceTree: "BUILT_PRODUCTS_DIR", + }; + const productFile = xcodeProject.addProductFile(targetName, options); + return productFile; +} diff --git a/plugin/build/xcode/addTargetDependency.d.ts b/plugin/build/xcode/addTargetDependency.d.ts new file mode 100644 index 0000000..a4141d4 --- /dev/null +++ b/plugin/build/xcode/addTargetDependency.d.ts @@ -0,0 +1,4 @@ +import type { XcodeProject } from "expo/config-plugins"; +export declare function addTargetDependency(xcodeProject: XcodeProject, target: { + uuid: string; +}): void; diff --git a/plugin/build/xcode/addTargetDependency.js b/plugin/build/xcode/addTargetDependency.js new file mode 100644 index 0000000..8ebf2fb --- /dev/null +++ b/plugin/build/xcode/addTargetDependency.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addTargetDependency = addTargetDependency; +function addTargetDependency(xcodeProject, target) { + if (!xcodeProject.hash.project.objects.PBXTargetDependency) { + xcodeProject.hash.project.objects.PBXTargetDependency = {}; + } + if (!xcodeProject.hash.project.objects.PBXContainerItemProxy) { + xcodeProject.hash.project.objects.PBXContainerItemProxy = {}; + } + xcodeProject.addTargetDependency(xcodeProject.getFirstTarget().uuid, [ + target.uuid, + ]); +} diff --git a/plugin/build/xcode/addToPbxNativeTargetSection.d.ts b/plugin/build/xcode/addToPbxNativeTargetSection.d.ts new file mode 100644 index 0000000..353430c --- /dev/null +++ b/plugin/build/xcode/addToPbxNativeTargetSection.d.ts @@ -0,0 +1,24 @@ +import type { XcodeProject } from "expo/config-plugins"; +export declare function addToPbxNativeTargetSection(xcodeProject: XcodeProject, { targetName, targetUuid, productFile, xCConfigurationList, }: { + targetName: string; + targetUuid: string; + productFile: { + fileRef: string; + }; + xCConfigurationList: { + uuid: string; + }; +}): { + uuid: string; + pbxNativeTarget: { + isa: string; + name: string; + productName: string; + productReference: string; + productType: string; + buildConfigurationList: string; + buildPhases: never[]; + buildRules: never[]; + dependencies: never[]; + }; +}; diff --git a/plugin/build/xcode/addToPbxNativeTargetSection.js b/plugin/build/xcode/addToPbxNativeTargetSection.js new file mode 100644 index 0000000..5631f9b --- /dev/null +++ b/plugin/build/xcode/addToPbxNativeTargetSection.js @@ -0,0 +1,21 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addToPbxNativeTargetSection = addToPbxNativeTargetSection; +function addToPbxNativeTargetSection(xcodeProject, { targetName, targetUuid, productFile, xCConfigurationList, }) { + const target = { + uuid: targetUuid, + pbxNativeTarget: { + isa: "PBXNativeTarget", + name: targetName, + productName: targetName, + productReference: productFile.fileRef, + productType: `"com.apple.product-type.application.on-demand-install-capable"`, + buildConfigurationList: xCConfigurationList.uuid, + buildPhases: [], + buildRules: [], + dependencies: [], + }, + }; + xcodeProject.addToPbxNativeTargetSection(target); + return target; +} diff --git a/plugin/build/xcode/addToPbxProjectSection.d.ts b/plugin/build/xcode/addToPbxProjectSection.d.ts new file mode 100644 index 0000000..c32cf23 --- /dev/null +++ b/plugin/build/xcode/addToPbxProjectSection.d.ts @@ -0,0 +1,4 @@ +import type { XcodeProject } from "expo/config-plugins"; +export declare function addToPbxProjectSection(xcodeProject: XcodeProject, target: { + uuid: string; +}): void; diff --git a/plugin/build/xcode/addToPbxProjectSection.js b/plugin/build/xcode/addToPbxProjectSection.js new file mode 100644 index 0000000..c7639c7 --- /dev/null +++ b/plugin/build/xcode/addToPbxProjectSection.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addToPbxProjectSection = addToPbxProjectSection; +function addToPbxProjectSection(xcodeProject, target) { + xcodeProject.addToPbxProjectSection(target); + // Add target attributes to project section + if (!xcodeProject.pbxProjectSection()[xcodeProject.getFirstProject().uuid] + .attributes.TargetAttributes) { + xcodeProject.pbxProjectSection()[xcodeProject.getFirstProject().uuid].attributes.TargetAttributes = {}; + } + xcodeProject.pbxProjectSection()[xcodeProject.getFirstProject().uuid].attributes.TargetAttributes[target.uuid] = { + LastSwiftMigration: 1250, + }; +} diff --git a/plugin/build/xcode/addXCConfigurationList.d.ts b/plugin/build/xcode/addXCConfigurationList.d.ts new file mode 100644 index 0000000..14f96c7 --- /dev/null +++ b/plugin/build/xcode/addXCConfigurationList.d.ts @@ -0,0 +1,10 @@ +import type { XcodeProject } from "expo/config-plugins"; +export declare function addXCConfigurationList(xcodeProject: XcodeProject, { name, targetName, currentProjectVersion, bundleIdentifier, deploymentTarget, appleTeamId, appIconName, }: { + name: string; + targetName: string; + currentProjectVersion: string; + bundleIdentifier: string; + deploymentTarget: string; + appleTeamId?: string; + appIconName?: string; +}): any; diff --git a/plugin/build/xcode/addXCConfigurationList.js b/plugin/build/xcode/addXCConfigurationList.js new file mode 100644 index 0000000..38c1057 --- /dev/null +++ b/plugin/build/xcode/addXCConfigurationList.js @@ -0,0 +1,41 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addXCConfigurationList = addXCConfigurationList; +function addXCConfigurationList(xcodeProject, { name, targetName, currentProjectVersion, bundleIdentifier, deploymentTarget, appleTeamId, appIconName, }) { + const commonBuildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME: appIconName + ? JSON.stringify(appIconName) + : "AppIcon", + CLANG_ENABLE_MODULES: "YES", + CURRENT_PROJECT_VERSION: `"${currentProjectVersion}"`, + INFOPLIST_FILE: `${targetName}/Info.plist`, + IPHONEOS_DEPLOYMENT_TARGET: `"${deploymentTarget}"`, + // LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks", + // OTHER_LDFLAGS: `("$(inherited)","-ObjC","-lc++",)`, + PRODUCT_BUNDLE_IDENTIFIER: `"${bundleIdentifier}"`, + PRODUCT_NAME: `"${name}"`, + SWIFT_VERSION: "5.0", + VERSIONING_SYSTEM: `"apple-generic"`, + // TARGETED_DEVICE_FAMILY: `"1,2"`, + CODE_SIGN_ENTITLEMENTS: `${targetName}/${targetName}.entitlements`, + ...(appleTeamId ? { DEVELOPMENT_TEAM: appleTeamId } : {}), + }; + const buildConfigurationsList = [ + { + name: "Debug", + isa: "XCBuildConfiguration", + buildSettings: { + ...commonBuildSettings, + }, + }, + { + name: "Release", + isa: "XCBuildConfiguration", + buildSettings: { + ...commonBuildSettings, + }, + }, + ]; + const xCConfigurationList = xcodeProject.addXCConfigurationList(buildConfigurationsList, "Release", `Build configuration list for PBXNativeTarget "${targetName}"`); + return xCConfigurationList; +} diff --git a/plugin/build/xcode/modifyAppDelegate.d.ts b/plugin/build/xcode/modifyAppDelegate.d.ts new file mode 100644 index 0000000..81ca6dc --- /dev/null +++ b/plugin/build/xcode/modifyAppDelegate.d.ts @@ -0,0 +1 @@ +export declare function modifyAppDelegate(appDelegatePath: string): void; diff --git a/plugin/build/xcode/modifyAppDelegate.js b/plugin/build/xcode/modifyAppDelegate.js new file mode 100644 index 0000000..dd383d0 --- /dev/null +++ b/plugin/build/xcode/modifyAppDelegate.js @@ -0,0 +1,112 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.modifyAppDelegate = modifyAppDelegate; +const node_fs_1 = __importDefault(require("node:fs")); +function modifyAppDelegate(appDelegatePath) { + let content = node_fs_1.default.readFileSync(appDelegatePath, "utf8"); + // Check if already modified + if (content.includes("decompressGzipBundle")) { + console.log("✅ AppDelegate already configured for compression"); + return; + } + // Add zlib import at the top + if (!content.includes("import zlib")) { + content = content.replace(/(import.*\n)+/, (imports) => imports + "import zlib\n"); + } + // Find and replace the bundleURL() function + const bundleURLPattern = /override func bundleURL\(\) -> URL\? \{[\s\S]*?#if DEBUG[\s\S]*?#else[\s\S]*?#endif[\s\S]*?\}/; + if (bundleURLPattern.test(content)) { + content = content.replace(bundleURLPattern, `override func bundleURL() -> URL? { +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") +#else + return decompressedBundleURL() +#endif + } + + // MARK: - Bundle Decompression + + private func decompressedBundleURL() -> URL? { + if let gzPath = Bundle.main.url(forResource: "main.jsbundle", withExtension: "gz") { + return decompressGzipBundle(at: gzPath) + } + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") + } + + private func decompressGzipBundle(at compressedURL: URL) -> URL? { + let decompressedPath = NSTemporaryDirectory() + "main.jsbundle" + let decompressedURL = URL(fileURLWithPath: decompressedPath) + + try? FileManager.default.removeItem(at: decompressedURL) + + do { + let compressedData = try Data(contentsOf: compressedURL) + guard let decompressedData = compressedData.gunzipped() else { + print("❌ Failed to decompress bundle") + return nil + } + + try decompressedData.write(to: decompressedURL, options: .atomic) + print("✅ Decompressed: \\(compressedData.count / 1024) KB → \\(decompressedData.count / 1024) KB") + + return decompressedURL + } catch { + print("❌ Error: \\(error)") + return nil + } + } +} + +// MARK: - Gzip Extension +extension Data { + func gunzipped() -> Data? { + guard !self.isEmpty else { return self } + + var stream = z_stream() + var decompressed = Data(capacity: self.count * 2) + + let status: CInt = self.withUnsafeBytes { (inputPtr: UnsafeRawBufferPointer) -> CInt in + stream.avail_in = UInt32(self.count) + stream.next_in = UnsafeMutablePointer(mutating: inputPtr.bindMemory(to: UInt8.self).baseAddress!) + + guard inflateInit2_(&stream, 15 + 32, ZLIB_VERSION, Int32(MemoryLayout.size)) == Z_OK else { + return Z_STREAM_ERROR + } + + var result: CInt = Z_OK + repeat { + if Int(stream.total_out) >= decompressed.count { + decompressed.count += self.count / 2 + } + + let outputCount = decompressed.count + let totalOut = Int(stream.total_out) + + result = decompressed.withUnsafeMutableBytes { (outputPtr: UnsafeMutableRawBufferPointer) -> CInt in + stream.avail_out = UInt32(outputCount) - UInt32(totalOut) + stream.next_out = outputPtr.bindMemory(to: UInt8.self).baseAddress!.advanced(by: totalOut) + return inflate(&stream, Z_SYNC_FLUSH) + } + } while result == Z_OK + + inflateEnd(&stream) + return result + } + + guard status == Z_STREAM_END else { + return nil + } + + decompressed.count = Int(stream.total_out) + return decompressed + }`); + node_fs_1.default.writeFileSync(appDelegatePath, content); + console.log("✅ Modified AppDelegate for gzip decompression"); + } + else { + console.warn("⚠️ Could not find bundleURL() function in AppDelegate"); + } +} From e57d760afd0c475f1160d6dc474b100127699322 Mon Sep 17 00:00:00 2001 From: Kyrylo Orlov Date: Tue, 20 Oct 2026 12:39:35 -0400 Subject: [PATCH 4/4] Bump version to 0.9.3 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b2c858..b2101ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-app-clip", - "version": "0.9.2", + "version": "0.9.3", "description": "Config plugin to add an App Clip to a React Native iOS app", "main": "build/index.js", "types": "build/index.d.ts",