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); + }); + }, +});