customerio-expo-plugin 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/package.json +2 -1
  2. package/plugin/lib/commonjs/android/withAndroidManifestUpdates.js +64 -59
  3. package/plugin/lib/commonjs/android/withAndroidManifestUpdates.js.map +1 -1
  4. package/plugin/lib/commonjs/android/withAppGoogleServices.js +10 -7
  5. package/plugin/lib/commonjs/android/withAppGoogleServices.js.map +1 -1
  6. package/plugin/lib/commonjs/android/withGoogleServicesJSON.js +18 -21
  7. package/plugin/lib/commonjs/android/withGoogleServicesJSON.js.map +1 -1
  8. package/plugin/lib/commonjs/android/withLocationGradleProperties.js +16 -12
  9. package/plugin/lib/commonjs/android/withLocationGradleProperties.js.map +1 -1
  10. package/plugin/lib/commonjs/android/withMainApplicationModifications.js +19 -12
  11. package/plugin/lib/commonjs/android/withMainApplicationModifications.js.map +1 -1
  12. package/plugin/lib/commonjs/android/withNotificationChannelMetadata.js +2 -1
  13. package/plugin/lib/commonjs/android/withNotificationChannelMetadata.js.map +1 -1
  14. package/plugin/lib/commonjs/android/withProjectBuildGradle.js +29 -25
  15. package/plugin/lib/commonjs/android/withProjectBuildGradle.js.map +1 -1
  16. package/plugin/lib/commonjs/android/withProjectGoogleServices.js +9 -5
  17. package/plugin/lib/commonjs/android/withProjectGoogleServices.js.map +1 -1
  18. package/plugin/lib/commonjs/helpers/utils/injectCIOPodfileCode.js +63 -31
  19. package/plugin/lib/commonjs/helpers/utils/injectCIOPodfileCode.js.map +1 -1
  20. package/plugin/lib/commonjs/ios/withAppDelegateModifications.js +47 -33
  21. package/plugin/lib/commonjs/ios/withAppDelegateModifications.js.map +1 -1
  22. package/plugin/lib/commonjs/ios/withCIOIosSwift.js +26 -42
  23. package/plugin/lib/commonjs/ios/withCIOIosSwift.js.map +1 -1
  24. package/plugin/lib/commonjs/ios/withGoogleServicesJsonFile.js +46 -30
  25. package/plugin/lib/commonjs/ios/withGoogleServicesJsonFile.js.map +1 -1
  26. package/plugin/lib/commonjs/ios/withNotificationsXcodeProject.js +193 -123
  27. package/plugin/lib/commonjs/ios/withNotificationsXcodeProject.js.map +1 -1
  28. package/plugin/lib/module/android/withAndroidManifestUpdates.js +61 -58
  29. package/plugin/lib/module/android/withAndroidManifestUpdates.js.map +1 -1
  30. package/plugin/lib/module/android/withAppGoogleServices.js +9 -7
  31. package/plugin/lib/module/android/withAppGoogleServices.js.map +1 -1
  32. package/plugin/lib/module/android/withGoogleServicesJSON.js +17 -21
  33. package/plugin/lib/module/android/withGoogleServicesJSON.js.map +1 -1
  34. package/plugin/lib/module/android/withLocationGradleProperties.js +15 -12
  35. package/plugin/lib/module/android/withLocationGradleProperties.js.map +1 -1
  36. package/plugin/lib/module/android/withMainApplicationModifications.js +18 -12
  37. package/plugin/lib/module/android/withMainApplicationModifications.js.map +1 -1
  38. package/plugin/lib/module/android/withNotificationChannelMetadata.js +1 -1
  39. package/plugin/lib/module/android/withNotificationChannelMetadata.js.map +1 -1
  40. package/plugin/lib/module/android/withProjectBuildGradle.js +28 -25
  41. package/plugin/lib/module/android/withProjectBuildGradle.js.map +1 -1
  42. package/plugin/lib/module/android/withProjectGoogleServices.js +8 -5
  43. package/plugin/lib/module/android/withProjectGoogleServices.js.map +1 -1
  44. package/plugin/lib/module/helpers/utils/injectCIOPodfileCode.js +61 -31
  45. package/plugin/lib/module/helpers/utils/injectCIOPodfileCode.js.map +1 -1
  46. package/plugin/lib/module/ios/withAppDelegateModifications.js +45 -33
  47. package/plugin/lib/module/ios/withAppDelegateModifications.js.map +1 -1
  48. package/plugin/lib/module/ios/withCIOIosSwift.js +24 -42
  49. package/plugin/lib/module/ios/withCIOIosSwift.js.map +1 -1
  50. package/plugin/lib/module/ios/withGoogleServicesJsonFile.js +45 -30
  51. package/plugin/lib/module/ios/withGoogleServicesJsonFile.js.map +1 -1
  52. package/plugin/lib/module/ios/withNotificationsXcodeProject.js +188 -123
  53. package/plugin/lib/module/ios/withNotificationsXcodeProject.js.map +1 -1
  54. package/plugin/lib/typescript/android/withAndroidManifestUpdates.d.ts +2 -0
  55. package/plugin/lib/typescript/android/withAppGoogleServices.d.ts +1 -0
  56. package/plugin/lib/typescript/android/withGoogleServicesJSON.d.ts +1 -0
  57. package/plugin/lib/typescript/android/withLocationGradleProperties.d.ts +2 -0
  58. package/plugin/lib/typescript/android/withMainApplicationModifications.d.ts +6 -0
  59. package/plugin/lib/typescript/android/withNotificationChannelMetadata.d.ts +5 -0
  60. package/plugin/lib/typescript/android/withProjectBuildGradle.d.ts +9 -0
  61. package/plugin/lib/typescript/android/withProjectGoogleServices.d.ts +1 -0
  62. package/plugin/lib/typescript/helpers/utils/injectCIOPodfileCode.d.ts +14 -0
  63. package/plugin/lib/typescript/ios/withAppDelegateModifications.d.ts +13 -0
  64. package/plugin/lib/typescript/ios/withCIOIosSwift.d.ts +11 -0
  65. package/plugin/lib/typescript/ios/withGoogleServicesJsonFile.d.ts +14 -1
  66. package/plugin/lib/typescript/ios/withNotificationsXcodeProject.d.ts +53 -2
  67. package/plugin/src/android/withAndroidManifestUpdates.ts +83 -73
  68. package/plugin/src/android/withAppGoogleServices.ts +13 -11
  69. package/plugin/src/android/withGoogleServicesJSON.ts +30 -28
  70. package/plugin/src/android/withLocationGradleProperties.ts +23 -17
  71. package/plugin/src/android/withMainApplicationModifications.ts +25 -15
  72. package/plugin/src/android/withNotificationChannelMetadata.ts +1 -1
  73. package/plugin/src/android/withProjectBuildGradle.ts +37 -27
  74. package/plugin/src/android/withProjectGoogleServices.ts +14 -9
  75. package/plugin/src/helpers/utils/injectCIOPodfileCode.ts +83 -48
  76. package/plugin/src/ios/withAppDelegateModifications.ts +61 -48
  77. package/plugin/src/ios/withCIOIosSwift.ts +33 -50
  78. package/plugin/src/ios/withGoogleServicesJsonFile.ts +66 -48
  79. package/plugin/src/ios/withNotificationsXcodeProject.ts +258 -208
@@ -5,38 +5,40 @@ import { logger } from '../utils/logger';
5
5
  import { FileManagement } from './../helpers/utils/fileManagement';
6
6
  import type { CustomerIOPluginOptionsAndroid } from './../types/cio-types';
7
7
 
8
- export const withGoogleServicesJSON: ConfigPlugin<
9
- CustomerIOPluginOptionsAndroid
10
- > = (configOuter, cioProps) => {
11
- return withProjectBuildGradle(configOuter, (props) => {
12
- const options: CustomerIOPluginOptionsAndroid = {
13
- androidPath: props.modRequest.platformProjectRoot,
14
- googleServicesFile: cioProps?.googleServicesFile,
15
- };
16
- const { androidPath, googleServicesFile } = options;
17
- if (!FileManagement.exists(`${androidPath}/app/google-services.json`)) {
18
- if (googleServicesFile && FileManagement.exists(googleServicesFile)) {
19
- try {
20
- FileManagement.copyFile(
21
- googleServicesFile,
22
- `${androidPath}/app/google-services.json`
23
- );
24
- } catch {
25
- logger.info(
26
- `There was an error copying your google-services.json file. You can copy it manually into ${androidPath}/app/google-services.json`
27
- );
28
- }
29
- } else {
30
- logger.info(
31
- `The Google Services file provided in ${googleServicesFile} doesn't seem to exist. You can copy it manually into ${androidPath}/app/google-services.json`
32
- );
33
- }
34
- } else {
8
+ export function copyGoogleServicesFile(
9
+ androidPath: string,
10
+ googleServicesFile: string | undefined
11
+ ): void {
12
+ const destination = `${androidPath}/app/google-services.json`;
13
+
14
+ if (FileManagement.exists(destination)) {
15
+ logger.info(`File already exists: ${destination}. Skipping...`);
16
+ return;
17
+ }
18
+
19
+ if (googleServicesFile && FileManagement.exists(googleServicesFile)) {
20
+ try {
21
+ FileManagement.copyFile(googleServicesFile, destination);
22
+ } catch {
35
23
  logger.info(
36
- `File already exists: ${androidPath}/app/google-services.json. Skipping...`
24
+ `There was an error copying your google-services.json file. You can copy it manually into ${destination}`
37
25
  );
38
26
  }
27
+ } else {
28
+ logger.info(
29
+ `The Google Services file provided in ${googleServicesFile} doesn't seem to exist. You can copy it manually into ${destination}`
30
+ );
31
+ }
32
+ }
39
33
 
34
+ export const withGoogleServicesJSON: ConfigPlugin<
35
+ CustomerIOPluginOptionsAndroid
36
+ > = (configOuter, cioProps) => {
37
+ return withProjectBuildGradle(configOuter, (props) => {
38
+ copyGoogleServicesFile(
39
+ props.modRequest.platformProjectRoot,
40
+ cioProps?.googleServicesFile
41
+ );
40
42
  return props;
41
43
  });
42
44
  };
@@ -6,6 +6,28 @@ import type { CustomerIOPluginLocationOptions } from '../types/cio-types';
6
6
 
7
7
  const CUSTOMERIO_LOCATION_ENABLED_KEY = 'customerio_location_enabled';
8
8
 
9
+ export function modifyGradleProperties(
10
+ items: PropertiesItem[]
11
+ ): PropertiesItem[] {
12
+ const existingIndex = items.findIndex(
13
+ (item) => item.type === 'property' && item.key === CUSTOMERIO_LOCATION_ENABLED_KEY
14
+ );
15
+
16
+ const newItem: PropertiesItem = {
17
+ type: 'property',
18
+ key: CUSTOMERIO_LOCATION_ENABLED_KEY,
19
+ value: 'true',
20
+ };
21
+
22
+ if (existingIndex >= 0) {
23
+ items[existingIndex] = newItem;
24
+ } else {
25
+ items.push(newItem);
26
+ }
27
+
28
+ return items;
29
+ }
30
+
9
31
  /**
10
32
  * Adds or updates customerio_location_enabled in android/gradle.properties when location.enabled is true.
11
33
  * The Customer.io React Native SDK reads this to enable the location native module.
@@ -19,23 +41,7 @@ export const withLocationGradleProperties: ConfigPlugin<{
19
41
 
20
42
  return withGradleProperties(config, (config) => {
21
43
  const items = config.modResults as PropertiesItem[];
22
- const existingIndex = items.findIndex(
23
- (item) => item.type === 'property' && item.key === CUSTOMERIO_LOCATION_ENABLED_KEY
24
- );
25
-
26
- const newItem: PropertiesItem = {
27
- type: 'property',
28
- key: CUSTOMERIO_LOCATION_ENABLED_KEY,
29
- value: 'true',
30
- };
31
-
32
- if (existingIndex >= 0) {
33
- items[existingIndex] = newItem;
34
- } else {
35
- items.push(newItem);
36
- }
37
-
38
- config.modResults = items;
44
+ config.modResults = modifyGradleProperties(items);
39
45
  return config;
40
46
  });
41
47
  };
@@ -36,6 +36,30 @@ const getLocationInitOptions = (
36
36
  trackingMode: sdkConfig?.location?.trackingMode,
37
37
  });
38
38
 
39
+ const SDK_INITIALIZER_CLASS = 'CustomerIOSDKInitializer';
40
+ const SDK_INITIALIZER_PACKAGE = 'io.customer.sdk.expo';
41
+ const SDK_INITIALIZER_FILE = `${SDK_INITIALIZER_CLASS}.kt`;
42
+ const SDK_INITIALIZER_IMPORT = `import ${SDK_INITIALIZER_PACKAGE}.${SDK_INITIALIZER_CLASS}`;
43
+
44
+ /**
45
+ * Pure string transform: given the existing MainApplication contents, returns the contents
46
+ * with the CustomerIOSDKInitializer import and onCreate call injected (idempotent — if the
47
+ * initialize call is already present, the call-injection step is skipped).
48
+ */
49
+ export function injectCustomerIOInitializerIntoMainApplication(
50
+ contents: string
51
+ ): string {
52
+ let next = addImportToFile(contents, SDK_INITIALIZER_IMPORT);
53
+ if (!next.includes(CIO_NATIVE_SDK_INITIALIZE_CALL)) {
54
+ next = addCodeToMethod(
55
+ next,
56
+ CIO_MAINAPPLICATION_ONCREATE_REGEX,
57
+ CIO_NATIVE_SDK_INITIALIZE_SNIPPET
58
+ );
59
+ }
60
+ return next;
61
+ }
62
+
39
63
  /**
40
64
  * Setup CustomerIOSDKInitializer for Android auto initialization
41
65
  */
@@ -44,30 +68,16 @@ const setupCustomerIOSDKInitializer = (
44
68
  sdkConfig: NativeSDKConfig,
45
69
  location?: CustomerIOPluginLocationOptions,
46
70
  ): string => {
47
- const SDK_INITIALIZER_CLASS = 'CustomerIOSDKInitializer';
48
- const SDK_INITIALIZER_PACKAGE = 'io.customer.sdk.expo';
49
-
50
- const SDK_INITIALIZER_FILE = `${SDK_INITIALIZER_CLASS}.kt`;
51
- const SDK_INITIALIZER_IMPORT = `import ${SDK_INITIALIZER_PACKAGE}.${SDK_INITIALIZER_CLASS}`;
52
-
53
71
  const locationOptions = getLocationInitOptions(location, sdkConfig);
54
- let content = config.modResults.contents;
55
72
 
56
73
  try {
57
74
  // Always regenerate the CustomerIOSDKInitializer file to reflect config changes
58
75
  copyTemplateFile(config, SDK_INITIALIZER_FILE, SDK_INITIALIZER_PACKAGE, (content) =>
59
76
  patchNativeSDKInitializer(content, PLATFORM.ANDROID, sdkConfig, locationOptions)
60
77
  );
61
- // Add import if not already present
62
- content = addImportToFile(content, SDK_INITIALIZER_IMPORT);
63
- // Add initialization code to onCreate if not already present
64
- if (!content.includes(CIO_NATIVE_SDK_INITIALIZE_CALL)) {
65
- content = addCodeToMethod(content, CIO_MAINAPPLICATION_ONCREATE_REGEX, CIO_NATIVE_SDK_INITIALIZE_SNIPPET);
66
- }
78
+ return injectCustomerIOInitializerIntoMainApplication(config.modResults.contents);
67
79
  } catch (error) {
68
80
  logger.warn(`Could not setup ${SDK_INITIALIZER_CLASS}:`, error);
69
81
  return config.modResults.contents;
70
82
  }
71
-
72
- return content;
73
83
  };
@@ -7,7 +7,7 @@ import type { CustomerIOPluginOptionsAndroid } from '../types/cio-types';
7
7
  /**
8
8
  * Adds a metadata entry to the Android manifest if it doesn't already exist
9
9
  */
10
- const addMetadataIfNotExists = (
10
+ export const addMetadataIfNotExists = (
11
11
  application: ManifestApplication,
12
12
  name: string,
13
13
  value: string
@@ -23,6 +23,40 @@ function shouldDisableAndroid16Support(
23
23
  return isExpoVersion53OrLower(config);
24
24
  }
25
25
 
26
+ /**
27
+ * Pure string transform: injects an androidx resolution-strategy block into the
28
+ * project-level build.gradle's `allprojects { ... }` section when
29
+ * `disableAndroid16Support` is true. Idempotent — returns input unchanged if the
30
+ * snippet is already present, or if the flag is false.
31
+ */
32
+ export function modifyProjectBuildGradleAndroid16Support(
33
+ contents: string,
34
+ options: { disableAndroid16Support: boolean }
35
+ ): string {
36
+ if (!options.disableAndroid16Support) {
37
+ return contents;
38
+ }
39
+
40
+ if (contents.includes('androidx.core:core-ktx:1.13.1')) {
41
+ return contents;
42
+ }
43
+
44
+ const resolutionStrategy = `
45
+ configurations.all {
46
+ resolutionStrategy {
47
+ // Disable Android 16 support by forcing older androidx versions
48
+ // Compatible with API 35 and AGP 8.8.2 (prevents API 36/AGP 8.9.1+ requirement)
49
+ force 'androidx.core:core-ktx:1.13.1'
50
+ force 'androidx.lifecycle:lifecycle-process:2.8.7'
51
+ }
52
+ }`;
53
+
54
+ return contents.replace(
55
+ /allprojects\s*\{/,
56
+ `allprojects {${resolutionStrategy}`
57
+ );
58
+ }
59
+
26
60
  /**
27
61
  * Adds dependency resolution strategy to force specific androidx versions.
28
62
  * This disables Android 16 support for apps using Expo SDK 53 or older gradle versions.
@@ -38,34 +72,10 @@ export function withProjectBuildGradle(
38
72
  androidOptions?: CustomerIOPluginOptionsAndroid
39
73
  ): ExpoConfig {
40
74
  return withExpoProjectBuildGradle(config, (config) => {
41
- const { modResults } = config;
42
-
43
- // Check if Android 16 support should be disabled
44
- if (!shouldDisableAndroid16Support(config, androidOptions)) {
45
- return config;
46
- }
47
-
48
- // Skip if already applied
49
- if (modResults.contents.includes('androidx.core:core-ktx:1.13.1')) {
50
- return config;
51
- }
52
-
53
- const resolutionStrategy = `
54
- configurations.all {
55
- resolutionStrategy {
56
- // Disable Android 16 support by forcing older androidx versions
57
- // Compatible with API 35 and AGP 8.8.2 (prevents API 36/AGP 8.9.1+ requirement)
58
- force 'androidx.core:core-ktx:1.13.1'
59
- force 'androidx.lifecycle:lifecycle-process:2.8.7'
60
- }
61
- }`;
62
-
63
- // Add resolution strategy inside allprojects block
64
- modResults.contents = modResults.contents.replace(
65
- /allprojects\s*\{/,
66
- `allprojects {${resolutionStrategy}`
75
+ config.modResults.contents = modifyProjectBuildGradleAndroid16Support(
76
+ config.modResults.contents,
77
+ { disableAndroid16Support: shouldDisableAndroid16Support(config, androidOptions) }
67
78
  );
68
-
69
79
  return config;
70
80
  });
71
81
  }
@@ -7,19 +7,24 @@ import {
7
7
  } from './../helpers/constants/android';
8
8
  import type { CustomerIOPluginOptionsAndroid } from './../types/cio-types';
9
9
 
10
+ export function modifyProjectBuildGradleForGoogleServices(contents: string): string {
11
+ const regex = new RegExp(CIO_PROJECT_GOOGLE_SNIPPET);
12
+ if (regex.test(contents)) {
13
+ return contents;
14
+ }
15
+ return contents.replace(
16
+ CIO_PROJECT_BUILDSCRIPTS_REGEX,
17
+ `$1\n${CIO_PROJECT_GOOGLE_SNIPPET}`
18
+ );
19
+ }
20
+
10
21
  export const withProjectGoogleServices: ConfigPlugin<
11
22
  CustomerIOPluginOptionsAndroid
12
23
  > = (configOuter) => {
13
24
  return withProjectBuildGradle(configOuter, (props) => {
14
- const regex = new RegExp(CIO_PROJECT_GOOGLE_SNIPPET);
15
- const match = props.modResults.contents.match(regex);
16
- if (!match) {
17
- props.modResults.contents = props.modResults.contents.replace(
18
- CIO_PROJECT_BUILDSCRIPTS_REGEX,
19
- `$1\n${CIO_PROJECT_GOOGLE_SNIPPET}`
20
- );
21
- }
22
-
25
+ props.modResults.contents = modifyProjectBuildGradleForGoogleServices(
26
+ props.modResults.contents
27
+ );
23
28
  return props;
24
29
  });
25
30
  };
@@ -42,73 +42,108 @@ export function buildHostAppPodSnippet(
42
42
  return `pod 'customerio-reactnative', :subspecs => ['${pushSubspec}', 'location'], :path => '${resolvedPath}'`;
43
43
  }
44
44
 
45
- export async function injectCIOPodfileCode(
45
+ const HOST_APP_BLOCK_START = '# --- CustomerIO Host App START ---';
46
+ const HOST_APP_BLOCK_END = '# --- CustomerIO Host App END ---';
47
+ const NOTIFICATION_BLOCK_START = '# --- CustomerIO Notification START ---';
48
+ const NOTIFICATION_BLOCK_END = '# --- CustomerIO Notification END ---';
49
+
50
+ /**
51
+ * Pure string transform: given the existing Podfile contents, returns the
52
+ * Podfile with the CustomerIO host-app block injected before the Expo
53
+ * `post_install do |installer|` anchor. Idempotent — returns input unchanged
54
+ * if the block is already present.
55
+ */
56
+ export function injectHostAppPodfileCode(
57
+ podfileContent: string,
46
58
  iosPath: string,
47
59
  isFcmPushProvider: boolean,
48
60
  options?: InjectCIOPodfileOptions
49
- ) {
50
- const blockStart = '# --- CustomerIO Host App START ---';
51
- const blockEnd = '# --- CustomerIO Host App END ---';
52
-
53
- const filename = `${iosPath}/Podfile`;
54
- const podfile = await FileManagement.read(filename);
55
- const matches = podfile.match(new RegExp(blockStart));
56
-
57
- if (!matches) {
58
- // We need to decide what line of code in the Podfile to insert our native code.
59
- // The "post_install" line is always present in an Expo project Podfile so it's reliable.
60
- // Find that line in the Podfile and then we will insert our code above that line.
61
- const lineInPodfileToInjectSnippetBefore = /post_install do \|installer\|/;
61
+ ): string {
62
+ if (podfileContent.match(new RegExp(HOST_APP_BLOCK_START))) {
63
+ return podfileContent;
64
+ }
62
65
 
63
- const podLine = buildHostAppPodSnippet(iosPath, isFcmPushProvider, options);
66
+ // We need to decide what line of code in the Podfile to insert our native code.
67
+ // The "post_install" line is always present in an Expo project Podfile so it's reliable.
68
+ // Find that line in the Podfile and then we will insert our code above that line.
69
+ const lineInPodfileToInjectSnippetBefore = /post_install do \|installer\|/;
70
+ const podLine = buildHostAppPodSnippet(iosPath, isFcmPushProvider, options);
64
71
 
65
- const snippetToInjectInPodfile = `
66
- ${blockStart}
72
+ const snippetToInjectInPodfile = `
73
+ ${HOST_APP_BLOCK_START}
67
74
  ${podLine}
68
- ${blockEnd}
75
+ ${HOST_APP_BLOCK_END}
69
76
  `.trim();
70
77
 
71
- FileManagement.write(
72
- filename,
73
- injectCodeByRegex(
74
- podfile,
75
- lineInPodfileToInjectSnippetBefore,
76
- snippetToInjectInPodfile
77
- ).join('\n')
78
- );
79
- } else {
80
- logger.info('CustomerIO Podfile snippets already exists. Skipping...');
81
- }
78
+ return injectCodeByRegex(
79
+ podfileContent,
80
+ lineInPodfileToInjectSnippetBefore,
81
+ snippetToInjectInPodfile,
82
+ ).join('\n');
82
83
  }
83
84
 
84
- export async function injectCIONotificationPodfileCode(
85
+ export async function injectCIOPodfileCode(
85
86
  iosPath: string,
86
- useFrameworks: CustomerIOPluginOptionsIOS['useFrameworks'],
87
- isFcmPushProvider: boolean
87
+ isFcmPushProvider: boolean,
88
+ options?: InjectCIOPodfileOptions
88
89
  ) {
89
90
  const filename = `${iosPath}/Podfile`;
90
91
  const podfile = await FileManagement.read(filename);
92
+ const next = injectHostAppPodfileCode(podfile, iosPath, isFcmPushProvider, options);
93
+ if (next !== podfile) {
94
+ FileManagement.write(filename, next);
95
+ } else {
96
+ logger.info('CustomerIO Podfile snippets already exists. Skipping...');
97
+ }
98
+ }
91
99
 
92
- const blockStart = '# --- CustomerIO Notification START ---';
93
- const blockEnd = '# --- CustomerIO Notification END ---';
94
-
95
- const matches = podfile.match(new RegExp(blockStart));
96
-
97
- if (!matches) {
98
- const resolvedPath = getRelativePathToRNSDK(iosPath);
99
- const subspec = isFcmPushProvider ? 'fcm' : 'apn';
100
- const useFrameworksLine =
101
- useFrameworks === 'static' ? 'use_frameworks! :linkage => :static' : '';
100
+ /**
101
+ * Pure string transform: given the existing Podfile contents, returns the
102
+ * Podfile with the rich-push NotificationService target block appended at
103
+ * the end. Idempotent — returns input unchanged if the block is already
104
+ * present.
105
+ */
106
+ export function appendNotificationTargetToPodfile(
107
+ podfileContent: string,
108
+ iosPath: string,
109
+ isFcmPushProvider: boolean,
110
+ useFrameworks: CustomerIOPluginOptionsIOS['useFrameworks'],
111
+ ): string {
112
+ if (podfileContent.match(new RegExp(NOTIFICATION_BLOCK_START))) {
113
+ return podfileContent;
114
+ }
102
115
 
103
- const snippetToInjectInPodfile = `
104
- ${blockStart}
116
+ const snippetToAppend = `
117
+ ${NOTIFICATION_BLOCK_START}
105
118
  target 'NotificationService' do
106
- ${useFrameworksLine}
107
- pod 'customerio-reactnative-richpush/${subspec}', :path => '${resolvedPath}'
119
+ ${useFrameworks === 'static' ? 'use_frameworks! :linkage => :static' : ''}
120
+ pod 'customerio-reactnative-richpush/${isFcmPushProvider ? 'fcm' : 'apn'}', :path => '${getRelativePathToRNSDK(iosPath)}'
108
121
  end
109
- ${blockEnd}
122
+ ${NOTIFICATION_BLOCK_END}
110
123
  `.trim();
111
124
 
112
- FileManagement.append(filename, snippetToInjectInPodfile);
125
+ // Mirror FileManagement.append: append directly with no separator (real
126
+ // Podfiles end with a trailing newline, so the appended block starts on a
127
+ // fresh line in practice).
128
+ return `${podfileContent}${snippetToAppend}`;
129
+ }
130
+
131
+ export async function injectCIONotificationPodfileCode(
132
+ iosPath: string,
133
+ useFrameworks: CustomerIOPluginOptionsIOS['useFrameworks'],
134
+ isFcmPushProvider: boolean
135
+ ) {
136
+ const filename = `${iosPath}/Podfile`;
137
+ const podfile = await FileManagement.read(filename);
138
+ const next = appendNotificationTargetToPodfile(
139
+ podfile,
140
+ iosPath,
141
+ isFcmPushProvider,
142
+ useFrameworks,
143
+ );
144
+ if (next !== podfile) {
145
+ // FileManagement.append matches what the previous direct-append did.
146
+ // Slice off the leading content (already on disk) and append only the new tail.
147
+ FileManagement.append(filename, next.slice(podfile.length));
113
148
  }
114
149
  }
@@ -153,9 +153,12 @@ const addFirebaseDelegateForwardDeclarationIfNeeded = (
153
153
  return stringContents;
154
154
  };
155
155
 
156
- const addAppdelegateHeaderModification = (stringContents: string) => {
157
- // Add UNUserNotificationCenterDelegate if needed
158
- stringContents = stringContents.replace(
156
+ /**
157
+ * Pure string transform: ensures the AppDelegate header (Objective-C path) declares
158
+ * `UNUserNotificationCenterDelegate` and imports `UserNotifications`. Idempotent.
159
+ */
160
+ export function modifyAppDelegateHeader(headerContent: string): string {
161
+ return headerContent.replace(
159
162
  CIO_APPDELEGATEHEADER_REGEX,
160
163
  (match, interfaceDeclaration, _groupedDelegates, existingDelegates) => {
161
164
  if (
@@ -179,9 +182,7 @@ ${interfaceDeclaration.trim()} <${CIO_APPDELEGATEHEADER_USER_NOTIFICATION_CENTER
179
182
  }
180
183
  }
181
184
  );
182
-
183
- return stringContents;
184
- };
185
+ }
185
186
 
186
187
  const addHandleDeeplinkInKilledState = (stringContents: string) => {
187
188
  // Find if the deep link code snippet is already present
@@ -219,59 +220,71 @@ const addHandleDeeplinkInKilledState = (stringContents: string) => {
219
220
  return stringContents;
220
221
  };
221
222
 
223
+ /**
224
+ * Pure string transform: produces the modified Objective-C AppDelegate.m / AppDelegate.mm
225
+ * contents wired with the Customer.io push pipeline (imports, declarations, notification
226
+ * configuration, registration callbacks, optional killed-state deep-link, FCM forward decl,
227
+ * Expo notifications header). The caller is responsible for the AppDelegate header file
228
+ * (.h) — see `modifyAppDelegateHeader`.
229
+ */
230
+ export function modifyAppDelegateContents(
231
+ contents: string,
232
+ projectName: string,
233
+ props: CustomerIOPluginOptionsIOS
234
+ ): string {
235
+ let next = addImport(contents, projectName);
236
+ next = addNotificationHandlerDeclaration(next);
237
+
238
+ // unless this property is explicity set to true, push notification
239
+ // registration will be added to the AppDelegate
240
+ if (props.pushNotification?.disableNotificationRegistration !== true) {
241
+ next = addNotificationConfiguration(next);
242
+ }
243
+
244
+ next = addInitializeNativeCioSdk(next);
245
+
246
+ if (props.pushNotification?.handleDeeplinkInKilledState === true) {
247
+ next = addHandleDeeplinkInKilledState(next);
248
+ }
249
+
250
+ next = addDidFailToRegisterForRemoteNotificationsWithError(next);
251
+ next = addDidRegisterForRemoteNotificationsWithDeviceToken(next);
252
+
253
+ if (isFcmPushProvider(props)) {
254
+ next = addFirebaseDelegateForwardDeclarationIfNeeded(next);
255
+ }
256
+
257
+ next = addExpoNotificationsHeaderModification(next);
258
+
259
+ return next;
260
+ }
261
+
222
262
  export const withAppDelegateModifications: ConfigPlugin<
223
263
  CustomerIOPluginOptionsIOS
224
264
  > = (configOuter, props) => {
225
265
  return withAppDelegate(configOuter, async (config) => {
226
- let stringContents = config.modResults.contents;
266
+ const stringContents = config.modResults.contents;
227
267
  const regex = new RegExp(
228
268
  `#import <${config.modRequest.projectName}-Swift.h>`
229
269
  );
230
- const match = stringContents.match(regex);
231
-
232
- if (!match) {
233
- const headerPath = getAppDelegateHeaderFilePath(
234
- config.modRequest.projectRoot
235
- );
236
- let headerContent = await FileManagement.read(headerPath);
237
- headerContent = addAppdelegateHeaderModification(headerContent);
238
- FileManagement.write(headerPath, headerContent);
239
-
240
- stringContents = addImport(
241
- stringContents,
242
- config.modRequest.projectName as string
243
- );
244
- stringContents = addNotificationHandlerDeclaration(stringContents);
245
-
246
- // unless this property is explicity set to true, push notification
247
- // registration will be added to the AppDelegate
248
- if (props.pushNotification?.disableNotificationRegistration !== true) {
249
- stringContents = addNotificationConfiguration(stringContents);
250
- }
251
-
252
- stringContents = addInitializeNativeCioSdk(stringContents);
253
-
254
- if (props.pushNotification?.handleDeeplinkInKilledState === true) {
255
- stringContents = addHandleDeeplinkInKilledState(stringContents);
256
- }
257
-
258
- stringContents =
259
- addDidFailToRegisterForRemoteNotificationsWithError(stringContents);
260
- stringContents =
261
- addDidRegisterForRemoteNotificationsWithDeviceToken(stringContents);
262
-
263
- if (isFcmPushProvider(props)) {
264
- stringContents =
265
- addFirebaseDelegateForwardDeclarationIfNeeded(stringContents);
266
- }
267
-
268
- stringContents = addExpoNotificationsHeaderModification(stringContents);
269
270
 
270
- config.modResults.contents = stringContents;
271
- } else {
271
+ if (stringContents.match(regex)) {
272
272
  logger.info('Customerio AppDelegate changes already exist. Skipping...');
273
+ return config;
273
274
  }
274
275
 
276
+ const headerPath = getAppDelegateHeaderFilePath(
277
+ config.modRequest.projectRoot
278
+ );
279
+ const headerContent = await FileManagement.read(headerPath);
280
+ FileManagement.write(headerPath, modifyAppDelegateHeader(headerContent));
281
+
282
+ config.modResults.contents = modifyAppDelegateContents(
283
+ stringContents,
284
+ config.modRequest.projectName as string,
285
+ props
286
+ );
287
+
275
288
  return config;
276
289
  });
277
290
  };