diff --git a/apps/docs/src/content/docs/guides/build.mdx b/apps/docs/src/content/docs/guides/build.mdx
index 1bb2238ade..bb42fed7c0 100644
--- a/apps/docs/src/content/docs/guides/build.mdx
+++ b/apps/docs/src/content/docs/guides/build.mdx
@@ -70,6 +70,7 @@ type IOSConfig = {
* bundleId: "com.app",
* displayName: "App",
* entitlementsFilePath: "./path/to/app.entitlements",
+ * privacyManifestPath: "./path/to/PrivacyInfo.xcprivacy",
* frameworks: {
* framework: "Sprite.framework",
* },
@@ -109,6 +110,13 @@ type IOSConfig = {
*/
entitlementsFilePath?: string | undefined;
+ /**
+ * Optional PrivacyInfo.xcprivacy path relative to the root of the project.
+ *
+ * https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
+ */
+ privacyManifestPath?: string | undefined;
+
/**
* Optional frameworks.
*/
diff --git a/apps/docs/src/content/docs/packages/cli-kit.mdx b/apps/docs/src/content/docs/packages/cli-kit.mdx
index d5d7ad69e4..2cf708cf32 100644
--- a/apps/docs/src/content/docs/packages/cli-kit.mdx
+++ b/apps/docs/src/content/docs/packages/cli-kit.mdx
@@ -755,6 +755,24 @@ const entitlementsPath = path.ios.entitlements;
##### Constant
+###### `path.ios.privacyManifest`
+
+Generates the absolute path to the iOS PrivacyInfo.xcprivacy.
+
+**type:** `string`
+
+##### Usage
+
+Below is an illustrative example demonstrating the utilization of the `path.ios.privacyManifest` to generate the absolute path to the PrivacyInfo.xcprivacy.
+
+```ts
+import { path } from "@brandingbrand/code-cli-kit";
+
+const privacyManifestPath = path.ios.privacyManifest;
+```
+
+##### Constant
+
###### `path.ios.nativeConstants`
Generates the absolute path to the iOS NativeConstants.m.
diff --git a/packages/cli-kit/__tests__/path.ts b/packages/cli-kit/__tests__/path.ts
index 8f972cbdd7..d61e6ff8ec 100644
--- a/packages/cli-kit/__tests__/path.ts
+++ b/packages/cli-kit/__tests__/path.ts
@@ -29,6 +29,20 @@ describe("path", () => {
);
});
+ it("should have an ios.entitlements function that returns the path to ios/app/app.entitlements", () => {
+ const entitlementsPath = path.ios.entitlements;
+ expect(entitlementsPath).toEqual(
+ expect.stringMatching(/.*ios\/app\/app\.entitlements$/)
+ );
+ });
+
+ it("should have an ios.privacyManifest function that returns the path to ios/app/PrivacyInfo.xcprivacy", () => {
+ const privacyManifestPath = path.ios.privacyManifest;
+ expect(privacyManifestPath).toEqual(
+ expect.stringMatching(/.*ios\/app\/PrivacyInfo\.xcprivacy$/)
+ );
+ });
+
it("should have an ios.gemfile function that returns the path to ios/app/Gemfile", () => {
const gemfilePath = path.ios.gemfile;
expect(gemfilePath).toEqual(expect.stringMatching(/.*ios\/Gemfile$/));
diff --git a/packages/cli-kit/src/lib/path.ts b/packages/cli-kit/src/lib/path.ts
index 9d3f0b64e4..44be6d8533 100644
--- a/packages/cli-kit/src/lib/path.ts
+++ b/packages/cli-kit/src/lib/path.ts
@@ -95,6 +95,17 @@ export default {
*/
entitlements: resolvePathFromProject("ios", "app", "app.entitlements"),
+ /**
+ * Retrieves the absolute path to the iOS PrivacyInfo.xcprivacy file.
+ *
+ * @returns {string} The absolute path to "ios/app/PrivacyInfo.xcprivacy".
+ */
+ privacyManifest: resolvePathFromProject(
+ "ios",
+ "app",
+ "PrivacyInfo.xcprivacy"
+ ),
+
/**
* Retrieves the absolute path to the iOS NativeConstants.m file.
*
diff --git a/packages/cli-kit/src/schemas/build-config.ts b/packages/cli-kit/src/schemas/build-config.ts
index 5e4ad536bf..095207d5fe 100644
--- a/packages/cli-kit/src/schemas/build-config.ts
+++ b/packages/cli-kit/src/schemas/build-config.ts
@@ -312,6 +312,7 @@ const IOSSchema = t.exact(
* bundleId: "com.app",
* displayName: "App",
* entitlementsFilePath: "./path/to/app.entitlements",
+ * privacyManifestPath: "./path/to/PrivacyInfo.xcprivacy"
* frameworks: {
* framework: "Sprite.framework",
* },
@@ -352,6 +353,13 @@ const IOSSchema = t.exact(
*/
entitlementsFilePath: t.string,
+ /**
+ * Optional PrivacyInfo.xcprivacy path relative to the root of the project.
+ *
+ * https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
+ */
+ privacyManifestPath: t.string,
+
/**
* Optional frameworks.
*
diff --git a/packages/cli/__tests__/app-entitlements.ts b/packages/cli/__tests__/app-entitlements.ts
index e266563e7e..a785d7fa9b 100644
--- a/packages/cli/__tests__/app-entitlements.ts
+++ b/packages/cli/__tests__/app-entitlements.ts
@@ -8,7 +8,7 @@ import { type BuildConfig, fs, path } from "@brandingbrand/code-cli-kit";
import transformer from "../src/transformers/ios/app-entitlements";
-describe("ios project.pbxproj transformers", () => {
+describe("ios app.entitlements transformers", () => {
beforeEach(() => {
jest.resetAllMocks();
});
diff --git a/packages/cli/__tests__/privacy-info-xcprivacy.ts b/packages/cli/__tests__/privacy-info-xcprivacy.ts
new file mode 100644
index 0000000000..e146486e80
--- /dev/null
+++ b/packages/cli/__tests__/privacy-info-xcprivacy.ts
@@ -0,0 +1,70 @@
+/**
+ * @jest-environment-options {"requireTemplate": true, "fixtures": "privacy-info-xcprivacy_fixtures"}
+ */
+
+///
+
+import { type BuildConfig, fs, path } from "@brandingbrand/code-cli-kit";
+
+import transformer from "../src/transformers/ios/privacy-info-xcprivacy";
+
+describe("ios PrivacyInfo.xcprivacy transformers", () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it("should not update PrivacyInfo.xcprivacy file", async () => {
+ const config = {
+ ...__flagship_code_build_config,
+ } as BuildConfig;
+
+ const origionalContent = await fs.readFile(
+ path.ios.privacyManifest,
+ "utf-8"
+ );
+ await transformer.transform(config, {} as any);
+ const content = await fs.readFile(path.ios.privacyManifest, "utf-8");
+
+ expect(content).toEqual(origionalContent);
+ });
+
+ it("should update PrivacyInfo.xcprivacy file", async () => {
+ const config = {
+ ...__flagship_code_build_config,
+ } as BuildConfig;
+
+ config.ios.privacyManifestPath = "./PrivacyInfo.xcprivacy";
+
+ const privacyManifestContent = await fs.readFile(
+ path.project.resolve("PrivacyInfo.xcprivacy"),
+ "utf-8"
+ );
+
+ await transformer.transform(config, {} as any);
+ const content = await fs.readFile(path.ios.privacyManifest, "utf-8");
+
+ expect(content).toEqual(privacyManifestContent);
+ });
+
+ it("should throw error for wrong PrivacyInfo.xcprivacy path", async () => {
+ const config = {
+ ...__flagship_code_build_config,
+ } as BuildConfig;
+
+ config.ios.privacyManifestPath = "./blah/PrivacyInfo.xcprivacy";
+
+ const privacyManifestAbsolutePath = path.project.resolve(
+ config.ios.privacyManifestPath
+ );
+
+ const throwError = async () => {
+ await transformer.transform(config, {} as any);
+ };
+
+ await expect(throwError).rejects.toThrow(
+ new Error(
+ `[PrivacyInfoXCPrivacyTransformerError]: path to privacy manifest does not exist ${privacyManifestAbsolutePath}, please update privacyManifestPath to the correct path relative to the root of your React Native project.`
+ )
+ );
+ });
+});
diff --git a/packages/cli/__tests__/privacy-info-xcprivacy_fixtures/PrivacyInfo.xcprivacy b/packages/cli/__tests__/privacy-info-xcprivacy_fixtures/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000000..c82b6663d0
--- /dev/null
+++ b/packages/cli/__tests__/privacy-info-xcprivacy_fixtures/PrivacyInfo.xcprivacy
@@ -0,0 +1,30 @@
+
+
+
+
+ NSPrivacyCollectedDataTypes
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPITypeReasons
+
+ C617.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategorySystemBootTime
+ NSPrivacyAccessedAPITypeReasons
+
+ 35F9.1
+
+
+
+ NSPrivacyTracking
+
+
+
diff --git a/packages/cli/src/transformers/ios/app-entitlements.ts b/packages/cli/src/transformers/ios/app-entitlements.ts
index 416fd593c7..ddd9bea540 100644
--- a/packages/cli/src/transformers/ios/app-entitlements.ts
+++ b/packages/cli/src/transformers/ios/app-entitlements.ts
@@ -21,7 +21,7 @@ import { Transforms, defineTransformer } from "@/lib";
*/
export default defineTransformer>({
/**
- * The name of the file to be transformed ("build.gradle").
+ * The name of the file to be transformed ("app.entitlements").
* @type {string}
*/
file: "app.entitlements",
diff --git a/packages/cli/src/transformers/ios/index.ts b/packages/cli/src/transformers/ios/index.ts
index 9d34da62ee..21f0f39a5f 100644
--- a/packages/cli/src/transformers/ios/index.ts
+++ b/packages/cli/src/transformers/ios/index.ts
@@ -37,3 +37,8 @@ export { default as entitlements } from "./app-entitlements";
* Represents the AppDelegate.mm file transformers.
*/
export { default as appDelegate } from "./app-delegate-mm";
+
+/**
+ * Represents the PrivacyInfo.xcprivacy file transformers.
+ */
+export { default as privacyInfo } from "./privacy-info-xcprivacy";
diff --git a/packages/cli/src/transformers/ios/privacy-info-xcprivacy.ts b/packages/cli/src/transformers/ios/privacy-info-xcprivacy.ts
new file mode 100644
index 0000000000..85822766de
--- /dev/null
+++ b/packages/cli/src/transformers/ios/privacy-info-xcprivacy.ts
@@ -0,0 +1,79 @@
+import fs from "fs";
+
+import {
+ type BuildConfig,
+ type PrebuildOptions,
+ withUTF8,
+ path,
+ string,
+} from "@brandingbrand/code-cli-kit";
+
+import { Transforms, defineTransformer } from "@/lib";
+
+/**
+ * Defines a transformer for the iOS project's "PrivacyInfo.xcprivacy" file.
+ *
+ * @type {typeof defineTransformer<(content: string, config: BuildConfig) => string>} - The type of the transformer.
+ * @property {string} file - The name of the file to be transformed ("PrivacyInfo.xcprivacy").
+ * @property {Array<(content: string, config: BuildConfig) => string>} transforms - An array of transformer functions.
+ * @property {Function} transform - The main transform function that applies all specified transformations.
+ * @returns {Promise} The updated content of the "PrivacyInfo.xcprivacy" file.
+ */
+export default defineTransformer>({
+ /**
+ * The name of the file to be transformed ("PrivacyInfo.xcprivacy").
+ * @type {string}
+ */
+ file: "PrivacyInfo.xcprivacy",
+
+ /**
+ * An array of transformer functions to be applied to the "PrivacyInfo.xcprivacy" file.
+ * Each function receives the content of the file and the build configuration,
+ * and returns the updated content after applying specific transformations.
+ * @type {Array<(content: string, config: BuildConfig) => string>}
+ */
+ transforms: [
+ /**
+ * Transformer for updating the dependencies in the "PrivacyInfo.xcprivacy" file.
+ * @param {string} content - The content of the file.
+ * @param {BuildConfig} config - The build configuration.
+ * @returns {string} - The updated content.
+ */
+ (content: string, config: BuildConfig): string => {
+ const { privacyManifestPath } = config.ios;
+
+ if (!privacyManifestPath) return content;
+
+ const privacyManifestAbsolutePath =
+ path.project.resolve(privacyManifestPath);
+
+ if (!fs.existsSync(privacyManifestAbsolutePath)) {
+ throw new Error(
+ `[PrivacyInfoXCPrivacyTransformerError]: path to privacy manifest does not exist ${privacyManifestAbsolutePath}, please update privacyManifestPath to the correct path relative to the root of your React Native project.`
+ );
+ }
+
+ const privacyManifestContent = fs.readFileSync(
+ privacyManifestAbsolutePath,
+ "utf-8"
+ );
+
+ return string.replace(content, /[\s\S]*/m, privacyManifestContent);
+ },
+ ],
+ /**
+ * The main transform function that applies all specified transformations to the "PrivacyInfo.xcprivacy" file.
+ * @param {BuildConfig} config - The build configuration.
+ * @returns {Promise} - The updated content of the "PrivacyInfo.xcprivacy" file.
+ */
+ transform: async function (
+ config: BuildConfig,
+ options: PrebuildOptions
+ ): Promise {
+ return withUTF8(path.ios.privacyManifest, (content: string) => {
+ return this.transforms.reduce((acc, curr) => {
+ return curr(acc, config, options);
+ }, content);
+ });
+ },
+});