customerio-expo-plugin 3.1.0 → 3.3.0

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 (100) hide show
  1. package/package.json +3 -2
  2. package/plugin/lib/commonjs/android/withCIOAndroid.js +13 -2
  3. package/plugin/lib/commonjs/android/withCIOAndroid.js.map +1 -1
  4. package/plugin/lib/commonjs/android/withLocationGradleProperties.js +37 -0
  5. package/plugin/lib/commonjs/android/withLocationGradleProperties.js.map +1 -0
  6. package/plugin/lib/commonjs/android/withMainApplicationModifications.js +21 -5
  7. package/plugin/lib/commonjs/android/withMainApplicationModifications.js.map +1 -1
  8. package/plugin/lib/commonjs/helpers/native-files/android/CustomerIOSDKInitializer.kt +2 -0
  9. package/plugin/lib/commonjs/helpers/native-files/ios/CustomerIOSDKInitializer.swift +2 -0
  10. package/plugin/lib/commonjs/helpers/native-files/ios/apn/CioSdkAppDelegateHandler.swift +1 -1
  11. package/plugin/lib/commonjs/helpers/native-files/ios/apn/NotificationService.swift +1 -1
  12. package/plugin/lib/commonjs/helpers/native-files/ios/apn/PushService.swift +1 -1
  13. package/plugin/lib/commonjs/helpers/native-files/ios/fcm/CioSdkAppDelegateHandler.swift +1 -1
  14. package/plugin/lib/commonjs/helpers/native-files/ios/fcm/NotificationService.swift +1 -1
  15. package/plugin/lib/commonjs/helpers/native-files/ios/fcm/PushService.swift +1 -1
  16. package/plugin/lib/commonjs/helpers/utils/injectCIOPodfileCode.js +19 -2
  17. package/plugin/lib/commonjs/helpers/utils/injectCIOPodfileCode.js.map +1 -1
  18. package/plugin/lib/commonjs/helpers/utils/patchLocationCode.js +50 -0
  19. package/plugin/lib/commonjs/helpers/utils/patchLocationCode.js.map +1 -0
  20. package/plugin/lib/commonjs/helpers/utils/patchPluginNativeCode.js +3 -1
  21. package/plugin/lib/commonjs/helpers/utils/patchPluginNativeCode.js.map +1 -1
  22. package/plugin/lib/commonjs/index.js +2 -2
  23. package/plugin/lib/commonjs/index.js.map +1 -1
  24. package/plugin/lib/commonjs/ios/withCIOIos.js +46 -4
  25. package/plugin/lib/commonjs/ios/withCIOIos.js.map +1 -1
  26. package/plugin/lib/commonjs/ios/withCIOIosSwift.js +25 -12
  27. package/plugin/lib/commonjs/ios/withCIOIosSwift.js.map +1 -1
  28. package/plugin/lib/commonjs/ios/withNotificationsXcodeProject.js +45 -11
  29. package/plugin/lib/commonjs/ios/withNotificationsXcodeProject.js.map +1 -1
  30. package/plugin/lib/commonjs/ios/withXcodeProject.js +4 -1
  31. package/plugin/lib/commonjs/ios/withXcodeProject.js.map +1 -1
  32. package/plugin/lib/commonjs/types/cio-types.js.map +1 -1
  33. package/plugin/lib/commonjs/utils/validation.js +13 -0
  34. package/plugin/lib/commonjs/utils/validation.js.map +1 -1
  35. package/plugin/lib/module/android/withCIOAndroid.js +13 -2
  36. package/plugin/lib/module/android/withCIOAndroid.js.map +1 -1
  37. package/plugin/lib/module/android/withLocationGradleProperties.js +30 -0
  38. package/plugin/lib/module/android/withLocationGradleProperties.js.map +1 -0
  39. package/plugin/lib/module/android/withMainApplicationModifications.js +20 -4
  40. package/plugin/lib/module/android/withMainApplicationModifications.js.map +1 -1
  41. package/plugin/lib/module/helpers/native-files/android/CustomerIOSDKInitializer.kt +2 -0
  42. package/plugin/lib/module/helpers/native-files/ios/CustomerIOSDKInitializer.swift +2 -0
  43. package/plugin/lib/module/helpers/native-files/ios/apn/CioSdkAppDelegateHandler.swift +1 -1
  44. package/plugin/lib/module/helpers/native-files/ios/apn/NotificationService.swift +1 -1
  45. package/plugin/lib/module/helpers/native-files/ios/apn/PushService.swift +1 -1
  46. package/plugin/lib/module/helpers/native-files/ios/fcm/CioSdkAppDelegateHandler.swift +1 -1
  47. package/plugin/lib/module/helpers/native-files/ios/fcm/NotificationService.swift +1 -1
  48. package/plugin/lib/module/helpers/native-files/ios/fcm/PushService.swift +1 -1
  49. package/plugin/lib/module/helpers/utils/injectCIOPodfileCode.js +18 -2
  50. package/plugin/lib/module/helpers/utils/injectCIOPodfileCode.js.map +1 -1
  51. package/plugin/lib/module/helpers/utils/patchLocationCode.js +44 -0
  52. package/plugin/lib/module/helpers/utils/patchLocationCode.js.map +1 -0
  53. package/plugin/lib/module/helpers/utils/patchPluginNativeCode.js +3 -2
  54. package/plugin/lib/module/helpers/utils/patchPluginNativeCode.js.map +1 -1
  55. package/plugin/lib/module/index.js +2 -2
  56. package/plugin/lib/module/index.js.map +1 -1
  57. package/plugin/lib/module/ios/withCIOIos.js +46 -4
  58. package/plugin/lib/module/ios/withCIOIos.js.map +1 -1
  59. package/plugin/lib/module/ios/withCIOIosSwift.js +25 -12
  60. package/plugin/lib/module/ios/withCIOIosSwift.js.map +1 -1
  61. package/plugin/lib/module/ios/withNotificationsXcodeProject.js +45 -11
  62. package/plugin/lib/module/ios/withNotificationsXcodeProject.js.map +1 -1
  63. package/plugin/lib/module/ios/withXcodeProject.js +5 -1
  64. package/plugin/lib/module/ios/withXcodeProject.js.map +1 -1
  65. package/plugin/lib/module/types/cio-types.js.map +1 -1
  66. package/plugin/lib/module/utils/validation.js +13 -1
  67. package/plugin/lib/module/utils/validation.js.map +1 -1
  68. package/plugin/lib/typescript/android/withCIOAndroid.d.ts +2 -2
  69. package/plugin/lib/typescript/android/withLocationGradleProperties.d.ts +9 -0
  70. package/plugin/lib/typescript/android/withMainApplicationModifications.d.ts +7 -2
  71. package/plugin/lib/typescript/helpers/utils/injectCIOPodfileCode.d.ts +9 -1
  72. package/plugin/lib/typescript/helpers/utils/patchLocationCode.d.ts +12 -0
  73. package/plugin/lib/typescript/helpers/utils/patchPluginNativeCode.d.ts +3 -1
  74. package/plugin/lib/typescript/index.d.ts +2 -1
  75. package/plugin/lib/typescript/ios/withCIOIos.d.ts +2 -2
  76. package/plugin/lib/typescript/ios/withCIOIosSwift.d.ts +2 -2
  77. package/plugin/lib/typescript/ios/withXcodeProject.d.ts +8 -1
  78. package/plugin/lib/typescript/types/cio-types.d.ts +35 -0
  79. package/plugin/lib/typescript/utils/validation.d.ts +3 -2
  80. package/plugin/src/android/withCIOAndroid.ts +13 -2
  81. package/plugin/src/android/withLocationGradleProperties.ts +41 -0
  82. package/plugin/src/android/withMainApplicationModifications.ts +26 -4
  83. package/plugin/src/helpers/native-files/android/CustomerIOSDKInitializer.kt +2 -0
  84. package/plugin/src/helpers/native-files/ios/CustomerIOSDKInitializer.swift +2 -0
  85. package/plugin/src/helpers/native-files/ios/apn/CioSdkAppDelegateHandler.swift +1 -1
  86. package/plugin/src/helpers/native-files/ios/apn/NotificationService.swift +1 -1
  87. package/plugin/src/helpers/native-files/ios/apn/PushService.swift +1 -1
  88. package/plugin/src/helpers/native-files/ios/fcm/CioSdkAppDelegateHandler.swift +1 -1
  89. package/plugin/src/helpers/native-files/ios/fcm/NotificationService.swift +1 -1
  90. package/plugin/src/helpers/native-files/ios/fcm/PushService.swift +1 -1
  91. package/plugin/src/helpers/utils/injectCIOPodfileCode.ts +35 -3
  92. package/plugin/src/helpers/utils/patchLocationCode.ts +80 -0
  93. package/plugin/src/helpers/utils/patchPluginNativeCode.ts +10 -1
  94. package/plugin/src/index.ts +9 -3
  95. package/plugin/src/ios/withCIOIos.ts +40 -3
  96. package/plugin/src/ios/withCIOIosSwift.ts +30 -4
  97. package/plugin/src/ios/withNotificationsXcodeProject.ts +56 -1
  98. package/plugin/src/ios/withXcodeProject.ts +20 -3
  99. package/plugin/src/types/cio-types.ts +38 -0
  100. package/plugin/src/utils/validation.ts +18 -1
@@ -1,6 +1,7 @@
1
1
  import CioDataPipelines
2
2
  import CioInternalCommon
3
3
  import CioMessagingInApp
4
+ {{LOCATION_MODULE_IMPORT}}
4
5
 
5
6
  class CustomerIOSDKInitializer {
6
7
  static func initialize() {
@@ -23,6 +24,7 @@ class CustomerIOSDKInitializer {
23
24
  setIfDefined(value: {{SCREEN_VIEW_USE}}, thenPassItTo: builder.screenViewUse) { ScreenView.getScreenView($0) }
24
25
  setIfDefined(value: {{MIGRATION_SITE_ID}}, thenPassItTo: builder.migrationSiteId)
25
26
 
27
+ {{LOCATION_MODULE_INIT}}
26
28
  CustomerIO.initialize(withConfig: builder.build())
27
29
 
28
30
  if let siteId = siteId {
@@ -42,7 +42,7 @@ public class CioSdkAppDelegateHandler: NSObject {
42
42
  MessagingPushAPN.initialize(
43
43
  withConfig: MessagingPushConfigBuilder()
44
44
  .autoFetchDeviceToken({{AUTO_FETCH_DEVICE_TOKEN}})
45
- .showPushAppInForeground({{SHOW_PUSH_APP_IN_FOREGROUND}})
45
+ {{APP_GROUP_ID_BUILDER_LINE}} .showPushAppInForeground({{SHOW_PUSH_APP_IN_FOREGROUND}})
46
46
  .autoTrackPushEvents({{AUTO_TRACK_PUSH_EVENTS}})
47
47
  .build()
48
48
  )
@@ -12,7 +12,7 @@ public class NotificationServiceCioManager : NSObject {
12
12
  MessagingPushAPN.initializeForExtension(
13
13
  withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.customerIOCdpApiKey)
14
14
  .region(Env.customerIORegion)
15
- .build()
15
+ {{APP_GROUP_ID_BUILDER_LINE}} .build()
16
16
  )
17
17
 
18
18
  MessagingPush.shared.didReceive(request, withContentHandler: contentHandler)
@@ -15,7 +15,7 @@ public class CIOAppPushNotificationsHandler : NSObject {
15
15
  MessagingPushAPN.initialize(
16
16
  withConfig: MessagingPushConfigBuilder()
17
17
  .autoFetchDeviceToken({{AUTO_FETCH_DEVICE_TOKEN}})
18
- .showPushAppInForeground({{SHOW_PUSH_APP_IN_FOREGROUND}})
18
+ {{APP_GROUP_ID_BUILDER_LINE}} .showPushAppInForeground({{SHOW_PUSH_APP_IN_FOREGROUND}})
19
19
  .autoTrackPushEvents({{AUTO_TRACK_PUSH_EVENTS}})
20
20
  .build()
21
21
  )
@@ -52,7 +52,7 @@ public class CioSdkAppDelegateHandler: NSObject {
52
52
  MessagingPushFCM.initialize(
53
53
  withConfig: MessagingPushConfigBuilder()
54
54
  .autoFetchDeviceToken({{AUTO_FETCH_DEVICE_TOKEN}})
55
- .showPushAppInForeground({{SHOW_PUSH_APP_IN_FOREGROUND}})
55
+ {{APP_GROUP_ID_BUILDER_LINE}} .showPushAppInForeground({{SHOW_PUSH_APP_IN_FOREGROUND}})
56
56
  .autoTrackPushEvents({{AUTO_TRACK_PUSH_EVENTS}})
57
57
  .build()
58
58
  )
@@ -12,7 +12,7 @@ public class NotificationServiceCioManager : NSObject {
12
12
  MessagingPushFCM.initializeForExtension(
13
13
  withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.customerIOCdpApiKey)
14
14
  .region(Env.customerIORegion)
15
- .build()
15
+ {{APP_GROUP_ID_BUILDER_LINE}} .build()
16
16
  )
17
17
 
18
18
  MessagingPush.shared.didReceive(request, withContentHandler: contentHandler)
@@ -23,7 +23,7 @@ public class CIOAppPushNotificationsHandler : NSObject {
23
23
  MessagingPushFCM.initialize(
24
24
  withConfig: MessagingPushConfigBuilder()
25
25
  .autoFetchDeviceToken({{AUTO_FETCH_DEVICE_TOKEN}})
26
- .showPushAppInForeground({{SHOW_PUSH_APP_IN_FOREGROUND}})
26
+ {{APP_GROUP_ID_BUILDER_LINE}} .showPushAppInForeground({{SHOW_PUSH_APP_IN_FOREGROUND}})
27
27
  .autoTrackPushEvents({{AUTO_TRACK_PUSH_EVENTS}})
28
28
  .build()
29
29
  )
@@ -4,9 +4,40 @@ import { getRelativePathToRNSDK } from '../constants/ios';
4
4
  import { injectCodeByRegex } from './codeInjection';
5
5
  import { FileManagement } from './fileManagement';
6
6
 
7
+ export type InjectCIOPodfileOptions = {
8
+ /** When true, add the location subspec. When false/omit, use single push subspec only. */
9
+ locationEnabled?: boolean;
10
+ /** When false and locationEnabled, inject only :subspecs => ['location']. When true, use push + location. */
11
+ hasPush?: boolean;
12
+ };
13
+
14
+ /** Builds the host app pod line for the Podfile (single subspec or :subspecs with location). Exported for tests. */
15
+ export function buildHostAppPodSnippet(
16
+ iosPath: string,
17
+ isFcmPushProvider: boolean,
18
+ options?: InjectCIOPodfileOptions
19
+ ): string {
20
+ const path = getRelativePathToRNSDK(iosPath);
21
+ const locationEnabled = options?.locationEnabled === true;
22
+ const hasPush = options?.hasPush !== false;
23
+
24
+ if (!locationEnabled) {
25
+ const subspec = isFcmPushProvider ? 'fcm' : 'apn';
26
+ return `pod 'customerio-reactnative/${subspec}', :path => '${path}'`;
27
+ }
28
+
29
+ if (!hasPush) {
30
+ return `pod 'customerio-reactnative', :subspecs => ['location'], :path => '${path}'`;
31
+ }
32
+
33
+ const pushSubspec = isFcmPushProvider ? 'fcm' : 'apn';
34
+ return `pod 'customerio-reactnative', :subspecs => ['${pushSubspec}', 'location'], :path => '${path}'`;
35
+ }
36
+
7
37
  export async function injectCIOPodfileCode(
8
38
  iosPath: string,
9
- isFcmPushProvider: boolean
39
+ isFcmPushProvider: boolean,
40
+ options?: InjectCIOPodfileOptions
10
41
  ) {
11
42
  const blockStart = '# --- CustomerIO Host App START ---';
12
43
  const blockEnd = '# --- CustomerIO Host App END ---';
@@ -21,10 +52,11 @@ export async function injectCIOPodfileCode(
21
52
  // Find that line in the Podfile and then we will insert our code above that line.
22
53
  const lineInPodfileToInjectSnippetBefore = /post_install do \|installer\|/;
23
54
 
55
+ const podLine = buildHostAppPodSnippet(iosPath, isFcmPushProvider, options);
56
+
24
57
  const snippetToInjectInPodfile = `
25
58
  ${blockStart}
26
- pod 'customerio-reactnative/${isFcmPushProvider ? 'fcm' : 'apn'
27
- }', :path => '${getRelativePathToRNSDK(iosPath)}'
59
+ ${podLine}
28
60
  ${blockEnd}
29
61
  `.trim();
30
62
 
@@ -0,0 +1,80 @@
1
+ import type { LocationTrackingMode } from '../../types/cio-types';
2
+ import { PLATFORM, type Platform } from '../constants/common';
3
+
4
+ const VALID_TRACKING_MODES: LocationTrackingMode[] = ['OFF', 'MANUAL', 'ON_APP_START'];
5
+
6
+ /** Options for location module in generated native initializer */
7
+ export type LocationInitOptions = {
8
+ enabled: boolean;
9
+ trackingMode?: LocationTrackingMode;
10
+ };
11
+
12
+ function normalizeTrackingMode(
13
+ rawMode: string | undefined
14
+ ): LocationTrackingMode {
15
+ const upper = rawMode?.toUpperCase();
16
+ return upper && VALID_TRACKING_MODES.includes(upper as LocationTrackingMode)
17
+ ? (upper as LocationTrackingMode)
18
+ : 'MANUAL';
19
+ }
20
+
21
+ /**
22
+ * Replaces {{LOCATION_MODULE_IMPORT}} and {{LOCATION_MODULE_INIT}} placeholders
23
+ * in SDK initializer template content for the given platform.
24
+ */
25
+ export function patchLocationPlaceholders(
26
+ content: string,
27
+ platform: Platform,
28
+ locationOptions?: LocationInitOptions
29
+ ): string {
30
+ const locationEnabled = locationOptions?.enabled === true;
31
+ const trackingMode = normalizeTrackingMode(locationOptions?.trackingMode);
32
+
33
+ if (platform === PLATFORM.ANDROID) {
34
+ if (locationEnabled) {
35
+ return content
36
+ .replace(
37
+ /\{\{LOCATION_MODULE_IMPORT\}\}/g,
38
+ `import io.customer.location.LocationModuleConfig
39
+ import io.customer.location.LocationTrackingMode
40
+ import io.customer.location.ModuleLocation
41
+ `
42
+ )
43
+ .replace(
44
+ /\{\{LOCATION_MODULE_INIT\}\}/g,
45
+ `if (io.customer.reactnative.sdk.BuildConfig.CIO_LOCATION_ENABLED) {
46
+ addCustomerIOModule(
47
+ ModuleLocation(
48
+ LocationModuleConfig.Builder()
49
+ .setLocationTrackingMode(LocationTrackingMode.${trackingMode})
50
+ .build()
51
+ )
52
+ )
53
+ }
54
+ `
55
+ );
56
+ }
57
+ return content
58
+ .replace(/\n\{\{LOCATION_MODULE_IMPORT\}\}\n/g, '\n')
59
+ .replace(/\n\s*\{\{LOCATION_MODULE_INIT\}\}\n/g, '\n');
60
+ }
61
+
62
+ // iOS
63
+ if (locationEnabled) {
64
+ const modeSwift =
65
+ trackingMode === 'OFF'
66
+ ? '.off'
67
+ : trackingMode === 'ON_APP_START'
68
+ ? '.onAppStart'
69
+ : '.manual';
70
+ return content
71
+ .replace(/\{\{LOCATION_MODULE_IMPORT\}\}/g, 'import CioLocation\n')
72
+ .replace(
73
+ /\{\{LOCATION_MODULE_INIT\}\}/g,
74
+ `_ = builder.addModule(LocationModule(config: LocationConfig(mode: ${modeSwift})))`
75
+ );
76
+ }
77
+ return content
78
+ .replace(/\n\{\{LOCATION_MODULE_IMPORT\}\}\n/g, '\n')
79
+ .replace(/\n\s*\{\{LOCATION_MODULE_INIT\}\}\n/g, '\n\n');
80
+ }
@@ -2,6 +2,12 @@ import type { NativeSDKConfig } from '../../types/cio-types';
2
2
  import { getPluginVersion } from '../../utils/plugin';
3
3
  import { validateNativeSDKConfig } from '../../utils/validation';
4
4
  import { PLATFORM, type Platform } from '../constants/common';
5
+ import {
6
+ type LocationInitOptions,
7
+ patchLocationPlaceholders,
8
+ } from './patchLocationCode';
9
+
10
+ export type { LocationInitOptions };
5
11
 
6
12
  /**
7
13
  * Shared utility function to perform common SDK config replacements
@@ -10,7 +16,8 @@ import { PLATFORM, type Platform } from '../constants/common';
10
16
  export function patchNativeSDKInitializer(
11
17
  rawContent: string,
12
18
  platform: Platform,
13
- sdkConfig: NativeSDKConfig
19
+ sdkConfig: NativeSDKConfig,
20
+ locationOptions?: LocationInitOptions
14
21
  ): string {
15
22
  // Validate SDK configuration to ensure all fields are present and
16
23
  // correct at the time of patching in prebuild
@@ -98,5 +105,7 @@ export function patchNativeSDKInitializer(
98
105
  (configValue) => `"${configValue}"`
99
106
  );
100
107
 
108
+ content = patchLocationPlaceholders(content, platform, locationOptions);
109
+
101
110
  return content;
102
111
  }
@@ -3,7 +3,13 @@ import type { ExpoConfig } from '@expo/config-types';
3
3
  import { withCIOAndroid } from './android/withCIOAndroid';
4
4
  import { isExpoVersion53OrHigher } from './ios/utils';
5
5
  import { withCIOIos } from './ios/withCIOIos';
6
- import type { CustomerIOPluginOptions } from './types/cio-types';
6
+ import type {
7
+ CustomerIOPluginOptions,
8
+ LocationTrackingMode,
9
+ NativeSDKConfig,
10
+ } from './types/cio-types';
11
+
12
+ export type { LocationTrackingMode, NativeSDKConfig };
7
13
 
8
14
  // Entry point for config plugin
9
15
  function withCustomerIOPlugin(
@@ -20,8 +26,8 @@ function withCustomerIOPlugin(
20
26
  }
21
27
 
22
28
  // Apply platform specific modifications
23
- config = withCIOIos(config, props.config, props.ios);
24
- config = withCIOAndroid(config, props.config, props.android);
29
+ config = withCIOIos(config, props.config, props.ios, props.location);
30
+ config = withCIOAndroid(config, props.config, props.android, props.location);
25
31
 
26
32
  return config;
27
33
  }
@@ -1,11 +1,14 @@
1
1
  import type { ExpoConfig } from '@expo/config-types';
2
+ import { withEntitlementsPlist } from '@expo/config-plugins';
2
3
  import type {
3
4
  CustomerIOPluginOptionsIOS,
4
5
  CustomerIOPluginPushNotificationOptions,
6
+ CustomerIOPluginLocationOptions,
5
7
  NativeSDKConfig,
6
8
  } from '../types/cio-types';
7
9
  import { mergeConfigWithEnvValues } from '../utils/config';
8
10
  import { logger } from '../utils/logger';
11
+ import { validatePushNotificationOptions } from '../utils/validation';
9
12
  import { isExpoVersion53OrHigher } from './utils';
10
13
  import { withAppDelegateModifications } from './withAppDelegateModifications';
11
14
  import { withCIOIosSwift } from './withCIOIosSwift';
@@ -17,13 +20,16 @@ export function withCIOIos(
17
20
  config: ExpoConfig,
18
21
  sdkConfig?: NativeSDKConfig,
19
22
  props?: CustomerIOPluginOptionsIOS,
23
+ location?: CustomerIOPluginLocationOptions,
20
24
  ) {
21
25
  const isSwiftProject = isExpoVersion53OrHigher(config);
22
26
  const platformConfig = mergeDeprecatedPropertiesAndLogWarnings(props);
27
+ const locationEnabled = location?.enabled === true;
23
28
 
24
29
  if (platformConfig?.pushNotification) {
30
+ validatePushNotificationOptions(platformConfig.pushNotification);
25
31
  if (isSwiftProject) {
26
- config = withCIOIosSwift(config, sdkConfig, platformConfig);
32
+ config = withCIOIosSwift(config, sdkConfig, platformConfig, location);
27
33
  } else {
28
34
  // Auto initialization is only supported in Swift projects (Expo SDK 53+)
29
35
  // Legacy Objective-C projects only support push notifications
@@ -33,10 +39,41 @@ export function withCIOIos(
33
39
  platformConfig.pushNotification.env = platformConfig.pushNotification.env
34
40
  || mergeConfigWithEnvValues(platformConfig, sdkConfig);
35
41
  config = withCioNotificationsXcodeProject(config, platformConfig);
36
- config = withCioXcodeProject(config, platformConfig);
42
+ config = withCioXcodeProject(config, {
43
+ ...platformConfig,
44
+ podfileOptions: {
45
+ locationEnabled,
46
+ hasPush: true,
47
+ },
48
+ });
37
49
  config = withGoogleServicesJsonFile(config, platformConfig);
50
+
51
+ // Merge App Group entitlements on host only when appGroupId is explicitly set
52
+ const appGroupId = platformConfig.pushNotification?.appGroupId;
53
+ if (appGroupId) {
54
+ config = withEntitlementsPlist(config, (entitlementsConfig) => {
55
+ const entitlements = entitlementsConfig.modResults as Record<string, unknown>;
56
+ const existing = (entitlements['com.apple.security.application-groups'] as string[] | undefined) ?? [];
57
+ if (!existing.includes(appGroupId)) {
58
+ entitlements['com.apple.security.application-groups'] = [...existing, appGroupId];
59
+ }
60
+ return entitlementsConfig;
61
+ });
62
+ }
38
63
  } else if (sdkConfig && isSwiftProject) {
39
- config = withCIOIosSwift(config, sdkConfig, platformConfig);
64
+ config = withCIOIosSwift(config, sdkConfig, platformConfig, location);
65
+ if (locationEnabled) {
66
+ config = withCioXcodeProject(config, {
67
+ ...platformConfig,
68
+ podfileOptions: { locationEnabled: true, hasPush: false },
69
+ });
70
+ }
71
+ } else if (locationEnabled) {
72
+ // Location-only: no push, no config. Still add Podfile location subspec so CIO_LOCATION_ENABLED is set and native location code is included.
73
+ config = withCioXcodeProject(config, {
74
+ ...platformConfig,
75
+ podfileOptions: { locationEnabled: true, hasPush: false },
76
+ });
40
77
  }
41
78
 
42
79
  return config;
@@ -17,7 +17,11 @@ import {
17
17
  import { replaceCodeByRegex } from '../helpers/utils/codeInjection';
18
18
  import { FileManagement } from '../helpers/utils/fileManagement';
19
19
  import { patchNativeSDKInitializer } from '../helpers/utils/patchPluginNativeCode';
20
- import type { CustomerIOPluginOptionsIOS, NativeSDKConfig } from '../types/cio-types';
20
+ import type {
21
+ CustomerIOPluginOptionsIOS,
22
+ CustomerIOPluginLocationOptions,
23
+ NativeSDKConfig,
24
+ } from '../types/cio-types';
21
25
  import { logger } from '../utils/logger';
22
26
  import { getIosNativeFilesPath } from '../utils/plugin';
23
27
  import { copyFileToXcode, getOrCreateCustomerIOGroup } from '../utils/xcode';
@@ -34,6 +38,7 @@ const copyAndConfigureAppDelegateHandler = (
34
38
  config: ExportedConfigWithProps<XcodeProject>,
35
39
  sdkConfig?: NativeSDKConfig,
36
40
  props?: CustomerIOPluginOptionsIOS,
41
+ location?: CustomerIOPluginLocationOptions,
37
42
  ): ExportedConfigWithProps<XcodeProject> => {
38
43
  // Destination path in the iOS project
39
44
  const projectName = config.modRequest.projectName || '';
@@ -59,6 +64,7 @@ const copyAndConfigureAppDelegateHandler = (
59
64
  projectName,
60
65
  sdkConfig,
61
66
  props,
67
+ location,
62
68
  });
63
69
  } else if (sdkConfig) {
64
70
  // Copy only CustomerIOSDKInitializer.swift for auto-init without push notifications
@@ -68,6 +74,7 @@ const copyAndConfigureAppDelegateHandler = (
68
74
  iosProjectRoot,
69
75
  projectName,
70
76
  sdkConfig,
77
+ location,
71
78
  });
72
79
  }
73
80
 
@@ -81,6 +88,7 @@ const copyAndConfigurePushAppDelegateHandler = ({
81
88
  projectName,
82
89
  sdkConfig,
83
90
  props,
91
+ location,
84
92
  }: {
85
93
  xcodeProject: XcodeProject;
86
94
  group: XcodeProject['pbxCreateGroup'];
@@ -88,6 +96,7 @@ const copyAndConfigurePushAppDelegateHandler = ({
88
96
  projectName: string;
89
97
  sdkConfig: NativeSDKConfig | undefined;
90
98
  props: CustomerIOPluginOptionsIOS;
99
+ location?: CustomerIOPluginLocationOptions;
91
100
  }) => {
92
101
  const useFcm = isFcmPushProvider(props);
93
102
 
@@ -153,10 +162,20 @@ const copyAndConfigurePushAppDelegateHandler = ({
153
162
  showPushAppInForeground.toString()
154
163
  );
155
164
 
165
+ const appGroupId = props.pushNotification?.appGroupId;
166
+ const appGroupIdBuilderLine = appGroupId
167
+ ? ` .appGroupId(${JSON.stringify(appGroupId)})\n`
168
+ : '';
169
+ handlerFileContent = replaceCodeByRegex(
170
+ handlerFileContent,
171
+ /\{\{APP_GROUP_ID_BUILDER_LINE\}\}/,
172
+ appGroupIdBuilderLine
173
+ );
174
+
156
175
  // Add auto initialization if sdkConfig is provided
157
176
  if (sdkConfig) {
158
177
  // Also copy CustomerIOSDKInitializer.swift for auto-initialization
159
- copyAndConfigureNativeSDKInitializer({ xcodeProject, group, iosProjectRoot, projectName, sdkConfig });
178
+ copyAndConfigureNativeSDKInitializer({ xcodeProject, group, iosProjectRoot, projectName, sdkConfig, location });
160
179
 
161
180
  // Inject auto initialization call before MessagingPush initialization
162
181
  handlerFileContent = handlerFileContent.replace(CIO_MESSAGING_PUSH_APP_DELEGATE_INIT_REGEX, CIO_NATIVE_SDK_INITIALIZE_SNIPPET + '$1');
@@ -171,13 +190,18 @@ const copyAndConfigureNativeSDKInitializer = ({
171
190
  iosProjectRoot,
172
191
  projectName,
173
192
  sdkConfig,
193
+ location,
174
194
  }: {
175
195
  xcodeProject: XcodeProject;
176
196
  group: XcodeProject['pbxCreateGroup'];
177
197
  iosProjectRoot: string;
178
198
  projectName: string;
179
199
  sdkConfig: NativeSDKConfig;
200
+ location?: CustomerIOPluginLocationOptions;
180
201
  }) => {
202
+ const locationOptions = location
203
+ ? { enabled: location.enabled === true, trackingMode: sdkConfig?.location?.trackingMode }
204
+ : undefined;
181
205
  const filename = 'CustomerIOSDKInitializer.swift';
182
206
  const sourcePath = path.join(getIosNativeFilesPath(), filename);
183
207
  // Add the CustomerIOSDKInitializer.swift file to the same Xcode group as CioSdkAppDelegateHandler
@@ -187,7 +211,8 @@ const copyAndConfigureNativeSDKInitializer = ({
187
211
  projectName,
188
212
  sourceFilePath: sourcePath,
189
213
  targetFileName: filename,
190
- transform: (content) => patchNativeSDKInitializer(content, PLATFORM.IOS, sdkConfig),
214
+ transform: (content) =>
215
+ patchNativeSDKInitializer(content, PLATFORM.IOS, sdkConfig, locationOptions),
191
216
  customerIOGroup: group,
192
217
  });
193
218
  };
@@ -196,10 +221,11 @@ export const withCIOIosSwift = (
196
221
  configOuter: ExpoConfig,
197
222
  sdkConfig?: NativeSDKConfig,
198
223
  props?: CustomerIOPluginOptionsIOS,
224
+ location?: CustomerIOPluginLocationOptions,
199
225
  ) => {
200
226
  // First, copy required swift files to iOS folder and add it to Xcode project
201
227
  configOuter = withXcodeProject(configOuter, async (config) => {
202
- return copyAndConfigureAppDelegateHandler(config, sdkConfig, props);
228
+ return copyAndConfigureAppDelegateHandler(config, sdkConfig, props, location);
203
229
  });
204
230
 
205
231
  // Modify the AppDelegate based on configuration
@@ -135,6 +135,25 @@ const addRichPushXcodeProj = async (
135
135
 
136
136
  const platformSpecificFiles = ['NotificationService.swift'];
137
137
 
138
+ const nseEntitlementsFilename = 'NotificationService.entitlements';
139
+ const appGroupId = options.pushNotification?.appGroupId;
140
+
141
+ // Write NSE entitlements file only when appGroupId is explicitly configured
142
+ if (appGroupId) {
143
+ const nseEntitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
144
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
145
+ <plist version="1.0">
146
+ <dict>
147
+ <key>com.apple.security.application-groups</key>
148
+ <array>
149
+ <string>${appGroupId}</string>
150
+ </array>
151
+ </dict>
152
+ </plist>
153
+ `;
154
+ FileManagement.writeFile(`${nsePath}/${nseEntitlementsFilename}`, nseEntitlementsContent);
155
+ }
156
+
138
157
  const commonFiles = [
139
158
  PLIST_FILENAME,
140
159
  'NotificationService.h',
@@ -170,10 +189,19 @@ const addRichPushXcodeProj = async (
170
189
  infoPlistTargetFile,
171
190
  });
172
191
  updateNseEnv(getTargetFile(ENV_FILENAME), options.pushNotification?.env);
192
+ updateNseNotificationService(getTargetFile('NotificationService.swift'), options.pushNotification?.appGroupId);
193
+
194
+ // The entitlements file is generated (not copied from source), so it's listed separately
195
+ // for the Xcode group so it appears in the file navigator
196
+ const allGroupFiles = [
197
+ ...platformSpecificFiles,
198
+ ...commonFiles,
199
+ ...(appGroupId ? [nseEntitlementsFilename] : []),
200
+ ];
173
201
 
174
202
  // Create new PBXGroup for the extension
175
203
  const extGroup = xcodeProject.addPbxGroup(
176
- [...platformSpecificFiles, ...commonFiles], // Combine platform-specific and common files,
204
+ allGroupFiles,
177
205
  CIO_NOTIFICATION_TARGET_NAME,
178
206
  CIO_NOTIFICATION_TARGET_NAME
179
207
  );
@@ -247,6 +275,9 @@ const addRichPushXcodeProj = async (
247
275
  buildSettingsObj.TARGETED_DEVICE_FAMILY = TARGETED_DEVICE_FAMILY;
248
276
  buildSettingsObj.CODE_SIGN_STYLE = 'Automatic';
249
277
  buildSettingsObj.SWIFT_VERSION = 4.2;
278
+ if (appGroupId) {
279
+ buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `${CIO_NOTIFICATION_TARGET_NAME}/${nseEntitlementsFilename}`;
280
+ }
250
281
  }
251
282
  }
252
283
 
@@ -284,6 +315,20 @@ const updateNseInfoPlist = (payload: {
284
315
  FileManagement.writeFile(payload.infoPlistTargetFile, plistFileString);
285
316
  };
286
317
 
318
+ const updateNseNotificationService = (
319
+ notificationServiceFile: string,
320
+ appGroupId?: string,
321
+ ) => {
322
+ const APP_GROUP_ID_BUILDER_LINE_RE = /\{\{APP_GROUP_ID_BUILDER_LINE\}\}/;
323
+
324
+ let content = FileManagement.readFile(notificationServiceFile);
325
+ const builderLine = appGroupId
326
+ ? ` .appGroupId(${JSON.stringify(appGroupId)})\n`
327
+ : '';
328
+ content = replaceCodeByRegex(content, APP_GROUP_ID_BUILDER_LINE_RE, builderLine);
329
+ FileManagement.writeFile(notificationServiceFile, content);
330
+ };
331
+
287
332
  const updateNseEnv = (
288
333
  envFileName: string,
289
334
  richPushConfig?: RichPushConfig
@@ -429,5 +474,15 @@ const updatePushFile = (
429
474
  showPushAppInForeground.toString()
430
475
  );
431
476
 
477
+ const appGroupId = options.pushNotification?.appGroupId;
478
+ const appGroupIdBuilderLine = appGroupId
479
+ ? ` .appGroupId(${JSON.stringify(appGroupId)})\n`
480
+ : '';
481
+ envFileContent = replaceCodeByRegex(
482
+ envFileContent,
483
+ /\{\{APP_GROUP_ID_BUILDER_LINE\}\}/,
484
+ appGroupIdBuilderLine
485
+ );
486
+
432
487
  FileManagement.writeFile(envFileName, envFileContent);
433
488
  };
@@ -1,18 +1,35 @@
1
1
  import type { ConfigPlugin } from '@expo/config-plugins';
2
2
  import { withXcodeProject } from '@expo/config-plugins';
3
3
 
4
- import { injectCIOPodfileCode } from '../helpers/utils/injectCIOPodfileCode';
4
+ import {
5
+ injectCIOPodfileCode,
6
+ type InjectCIOPodfileOptions,
7
+ } from '../helpers/utils/injectCIOPodfileCode';
5
8
  import type { CustomerIOPluginOptionsIOS } from '../types/cio-types';
6
9
  import { isFcmPushProvider } from './utils';
7
10
 
8
- export const withCioXcodeProject: ConfigPlugin<CustomerIOPluginOptionsIOS> = (
11
+ export type WithCioXcodeProjectOptions = {
12
+ /** Options for Podfile host app snippet (location subspec, etc.) */
13
+ podfileOptions?: InjectCIOPodfileOptions;
14
+ };
15
+
16
+ /** Props for the CIO Xcode project mod; push options are optional when only location is enabled. */
17
+ export type WithCioXcodeProjectProps = Partial<CustomerIOPluginOptionsIOS> &
18
+ WithCioXcodeProjectOptions;
19
+
20
+ export const withCioXcodeProject: ConfigPlugin<WithCioXcodeProjectProps> = (
9
21
  config,
10
22
  cioProps
11
23
  ) => {
12
24
  return withXcodeProject(config, async (props) => {
13
25
  const iosPath = props.modRequest.platformProjectRoot;
26
+ const podfileOptions = cioProps?.podfileOptions;
14
27
 
15
- await injectCIOPodfileCode(iosPath, isFcmPushProvider(cioProps));
28
+ await injectCIOPodfileCode(
29
+ iosPath,
30
+ isFcmPushProvider(cioProps as CustomerIOPluginOptionsIOS | undefined),
31
+ podfileOptions
32
+ );
16
33
 
17
34
  return props;
18
35
  });
@@ -88,6 +88,13 @@ export type CustomerIOPluginOptionsAndroid = {
88
88
  disableAndroid16Support?: boolean;
89
89
  };
90
90
 
91
+ /**
92
+ * Location tracking mode for the Customer.io SDK location module.
93
+ * Location is off by default. Only used when location is enabled (plugin option location.enabled: true).
94
+ * @public
95
+ */
96
+ export type LocationTrackingMode = 'OFF' | 'MANUAL' | 'ON_APP_START';
97
+
91
98
  /**
92
99
  * SDK configuration options for auto initialization
93
100
  * @public
@@ -101,6 +108,24 @@ export type NativeSDKConfig = {
101
108
  logLevel?: 'none' | 'error' | 'info' | 'debug'; // Default: 'debug'. Controls SDK logging verbosity
102
109
  siteId?: string; // Optional, if only siteId defined, migrationSiteId = siteId
103
110
  migrationSiteId?: string; // Optional, if only migrationSiteId defined, siteId should be null
111
+ /**
112
+ * Location module config. Location is off by default; only applied when plugin option location.enabled is true.
113
+ * trackingMode: 'MANUAL' (host app controls when location is captured, default),
114
+ * 'ON_APP_START' (SDK captures once per launch when app becomes active), or 'OFF'.
115
+ */
116
+ location?: {
117
+ trackingMode?: LocationTrackingMode;
118
+ };
119
+ };
120
+
121
+ /**
122
+ * Location is off by default. When true, enables the Customer.io SDK location native module (iOS Podfile location subspec,
123
+ * Android gradle.properties flag). Permissions and privacy keys (Info.plist, AndroidManifest)
124
+ * remain the host app's responsibility.
125
+ * @public
126
+ */
127
+ export type CustomerIOPluginLocationOptions = {
128
+ enabled?: boolean;
104
129
  };
105
130
 
106
131
  /**
@@ -111,6 +136,11 @@ export type CustomerIOPluginOptions = {
111
136
  config?: NativeSDKConfig; // If defined, enables auto initialization of native SDK
112
137
  android: CustomerIOPluginOptionsAndroid;
113
138
  ios: CustomerIOPluginOptionsIOS;
139
+ /**
140
+ * Location is off by default. When location.enabled is true, the plugin adds SDK build-time setup (Podfile location subspec,
141
+ * gradle.properties). Host apps must add their own location permissions and privacy usage strings.
142
+ */
143
+ location?: CustomerIOPluginLocationOptions;
114
144
  };
115
145
 
116
146
  /**
@@ -141,4 +171,12 @@ export type CustomerIOPluginPushNotificationOptions = {
141
171
  * Optional if `config` is provided at the top level.
142
172
  */
143
173
  env?: RichPushConfig;
174
+
175
+ /**
176
+ * iOS App Group identifier shared between the host app and the Notification Service Extension.
177
+ * When set, `.appGroupId(...)` is injected into the MessagingPushConfigBuilder, the identifier
178
+ * is added to the host app entitlements, and an NSE entitlements file is written.
179
+ * When omitted, the native SDK handles group discovery on its own and no entitlements are added.
180
+ */
181
+ appGroupId?: string;
144
182
  };