customerio-expo-plugin 3.2.0 → 3.4.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.
- package/package.json +8 -2
- package/plugin/lib/commonjs/helpers/constants/ios.js +76 -8
- package/plugin/lib/commonjs/helpers/constants/ios.js.map +1 -1
- package/plugin/lib/commonjs/helpers/native-files/ios/apn/CioSdkAppDelegateHandler.swift +1 -1
- package/plugin/lib/commonjs/helpers/native-files/ios/apn/NotificationService.swift +1 -1
- package/plugin/lib/commonjs/helpers/native-files/ios/apn/PushService.swift +1 -1
- package/plugin/lib/commonjs/helpers/native-files/ios/fcm/CioSdkAppDelegateHandler.swift +1 -1
- package/plugin/lib/commonjs/helpers/native-files/ios/fcm/NotificationService.swift +1 -1
- package/plugin/lib/commonjs/helpers/native-files/ios/fcm/PushService.swift +1 -1
- package/plugin/lib/commonjs/helpers/utils/injectCIOPodfileCode.js +20 -7
- package/plugin/lib/commonjs/helpers/utils/injectCIOPodfileCode.js.map +1 -1
- package/plugin/lib/commonjs/index.js +7 -0
- package/plugin/lib/commonjs/index.js.map +1 -1
- package/plugin/lib/commonjs/ios/withCIOIos.js +17 -0
- package/plugin/lib/commonjs/ios/withCIOIos.js.map +1 -1
- package/plugin/lib/commonjs/ios/withCIOIosSwift.js +24 -15
- package/plugin/lib/commonjs/ios/withCIOIosSwift.js.map +1 -1
- package/plugin/lib/commonjs/ios/withNotificationsXcodeProject.js +45 -11
- package/plugin/lib/commonjs/ios/withNotificationsXcodeProject.js.map +1 -1
- package/plugin/lib/commonjs/postInstallHelper.js +58 -11
- package/plugin/lib/commonjs/postInstallHelper.js.map +1 -1
- package/plugin/lib/commonjs/types/cio-types.js.map +1 -1
- package/plugin/lib/commonjs/utils/resolveRNSDK.js +97 -0
- package/plugin/lib/commonjs/utils/resolveRNSDK.js.map +1 -0
- package/plugin/lib/commonjs/utils/validation.js +13 -0
- package/plugin/lib/commonjs/utils/validation.js.map +1 -1
- package/plugin/lib/commonjs/utils/writeExpoVersion.js +56 -0
- package/plugin/lib/commonjs/utils/writeExpoVersion.js.map +1 -0
- package/plugin/lib/module/helpers/constants/ios.js +75 -8
- package/plugin/lib/module/helpers/constants/ios.js.map +1 -1
- package/plugin/lib/module/helpers/native-files/ios/apn/CioSdkAppDelegateHandler.swift +1 -1
- package/plugin/lib/module/helpers/native-files/ios/apn/NotificationService.swift +1 -1
- package/plugin/lib/module/helpers/native-files/ios/apn/PushService.swift +1 -1
- package/plugin/lib/module/helpers/native-files/ios/fcm/CioSdkAppDelegateHandler.swift +1 -1
- package/plugin/lib/module/helpers/native-files/ios/fcm/NotificationService.swift +1 -1
- package/plugin/lib/module/helpers/native-files/ios/fcm/PushService.swift +1 -1
- package/plugin/lib/module/helpers/utils/injectCIOPodfileCode.js +20 -7
- package/plugin/lib/module/helpers/utils/injectCIOPodfileCode.js.map +1 -1
- package/plugin/lib/module/index.js +7 -0
- package/plugin/lib/module/index.js.map +1 -1
- package/plugin/lib/module/ios/withCIOIos.js +17 -0
- package/plugin/lib/module/ios/withCIOIos.js.map +1 -1
- package/plugin/lib/module/ios/withCIOIosSwift.js +24 -15
- package/plugin/lib/module/ios/withCIOIosSwift.js.map +1 -1
- package/plugin/lib/module/ios/withNotificationsXcodeProject.js +45 -11
- package/plugin/lib/module/ios/withNotificationsXcodeProject.js.map +1 -1
- package/plugin/lib/module/postInstallHelper.js +58 -11
- package/plugin/lib/module/postInstallHelper.js.map +1 -1
- package/plugin/lib/module/types/cio-types.js.map +1 -1
- package/plugin/lib/module/utils/resolveRNSDK.js +88 -0
- package/plugin/lib/module/utils/resolveRNSDK.js.map +1 -0
- package/plugin/lib/module/utils/validation.js +13 -1
- package/plugin/lib/module/utils/validation.js.map +1 -1
- package/plugin/lib/module/utils/writeExpoVersion.js +48 -0
- package/plugin/lib/module/utils/writeExpoVersion.js.map +1 -0
- package/plugin/lib/typescript/helpers/constants/ios.d.ts +18 -0
- package/plugin/lib/typescript/helpers/utils/injectCIOPodfileCode.d.ts +11 -1
- package/plugin/lib/typescript/types/cio-types.d.ts +7 -0
- package/plugin/lib/typescript/utils/resolveRNSDK.d.ts +7 -0
- package/plugin/lib/typescript/utils/validation.d.ts +3 -2
- package/plugin/lib/typescript/utils/writeExpoVersion.d.ts +3 -0
- package/plugin/src/helpers/constants/ios.ts +87 -8
- package/plugin/src/helpers/native-files/ios/apn/CioSdkAppDelegateHandler.swift +1 -1
- package/plugin/src/helpers/native-files/ios/apn/NotificationService.swift +1 -1
- package/plugin/src/helpers/native-files/ios/apn/PushService.swift +1 -1
- package/plugin/src/helpers/native-files/ios/fcm/CioSdkAppDelegateHandler.swift +1 -1
- package/plugin/src/helpers/native-files/ios/fcm/NotificationService.swift +1 -1
- package/plugin/src/helpers/native-files/ios/fcm/PushService.swift +1 -1
- package/plugin/src/helpers/utils/injectCIOPodfileCode.ts +22 -10
- package/plugin/src/index.ts +7 -0
- package/plugin/src/ios/withCIOIos.ts +16 -0
- package/plugin/src/ios/withCIOIosSwift.ts +35 -12
- package/plugin/src/ios/withNotificationsXcodeProject.ts +56 -1
- package/plugin/src/postInstallHelper.js +75 -17
- package/plugin/src/types/cio-types.ts +8 -0
- package/plugin/src/utils/resolveRNSDK.ts +118 -0
- package/plugin/src/utils/validation.ts +18 -1
- package/plugin/src/utils/writeExpoVersion.ts +62 -0
|
@@ -11,27 +11,35 @@ export type InjectCIOPodfileOptions = {
|
|
|
11
11
|
hasPush?: boolean;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
/** Builds the host
|
|
14
|
+
/** Builds the host-app pod snippet for the Podfile.
|
|
15
|
+
*
|
|
16
|
+
* The :path is resolved at prebuild time by `getRelativePathToRNSDK`,
|
|
17
|
+
* which dispatches on the installed React Native version so the path
|
|
18
|
+
* matches what RN pod autolinking will emit (lexical for RN <0.80,
|
|
19
|
+
* realpath for RN >=0.80). Baking the resolved string directly avoids
|
|
20
|
+
* any Ruby/install-time logic in the Podfile and keeps the snippet
|
|
21
|
+
* trivially diff-able.
|
|
22
|
+
*
|
|
23
|
+
* Exported for tests.
|
|
24
|
+
*/
|
|
15
25
|
export function buildHostAppPodSnippet(
|
|
16
26
|
iosPath: string,
|
|
17
27
|
isFcmPushProvider: boolean,
|
|
18
28
|
options?: InjectCIOPodfileOptions
|
|
19
29
|
): string {
|
|
20
|
-
const
|
|
30
|
+
const resolvedPath = getRelativePathToRNSDK(iosPath);
|
|
21
31
|
const locationEnabled = options?.locationEnabled === true;
|
|
22
32
|
const hasPush = options?.hasPush !== false;
|
|
23
33
|
|
|
24
34
|
if (!locationEnabled) {
|
|
25
35
|
const subspec = isFcmPushProvider ? 'fcm' : 'apn';
|
|
26
|
-
return `pod 'customerio-reactnative/${subspec}', :path => '${
|
|
36
|
+
return `pod 'customerio-reactnative/${subspec}', :path => '${resolvedPath}'`;
|
|
27
37
|
}
|
|
28
|
-
|
|
29
38
|
if (!hasPush) {
|
|
30
|
-
return `pod 'customerio-reactnative', :subspecs => ['location'], :path => '${
|
|
39
|
+
return `pod 'customerio-reactnative', :subspecs => ['location'], :path => '${resolvedPath}'`;
|
|
31
40
|
}
|
|
32
|
-
|
|
33
41
|
const pushSubspec = isFcmPushProvider ? 'fcm' : 'apn';
|
|
34
|
-
return `pod 'customerio-reactnative', :subspecs => ['${pushSubspec}', 'location'], :path => '${
|
|
42
|
+
return `pod 'customerio-reactnative', :subspecs => ['${pushSubspec}', 'location'], :path => '${resolvedPath}'`;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export async function injectCIOPodfileCode(
|
|
@@ -87,12 +95,16 @@ export async function injectCIONotificationPodfileCode(
|
|
|
87
95
|
const matches = podfile.match(new RegExp(blockStart));
|
|
88
96
|
|
|
89
97
|
if (!matches) {
|
|
98
|
+
const resolvedPath = getRelativePathToRNSDK(iosPath);
|
|
99
|
+
const subspec = isFcmPushProvider ? 'fcm' : 'apn';
|
|
100
|
+
const useFrameworksLine =
|
|
101
|
+
useFrameworks === 'static' ? 'use_frameworks! :linkage => :static' : '';
|
|
102
|
+
|
|
90
103
|
const snippetToInjectInPodfile = `
|
|
91
104
|
${blockStart}
|
|
92
105
|
target 'NotificationService' do
|
|
93
|
-
${
|
|
94
|
-
pod 'customerio-reactnative-richpush/${
|
|
95
|
-
}', :path => '${getRelativePathToRNSDK(iosPath)}'
|
|
106
|
+
${useFrameworksLine}
|
|
107
|
+
pod 'customerio-reactnative-richpush/${subspec}', :path => '${resolvedPath}'
|
|
96
108
|
end
|
|
97
109
|
${blockEnd}
|
|
98
110
|
`.trim();
|
package/plugin/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
LocationTrackingMode,
|
|
9
9
|
NativeSDKConfig,
|
|
10
10
|
} from './types/cio-types';
|
|
11
|
+
import { withExpoVersion } from './utils/writeExpoVersion';
|
|
11
12
|
|
|
12
13
|
export type { LocationTrackingMode, NativeSDKConfig };
|
|
13
14
|
|
|
@@ -25,6 +26,12 @@ function withCustomerIOPlugin(
|
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
// Belt-and-suspenders write of the plugin version into the RN SDK's
|
|
30
|
+
// package.json. The postinstall hook does the same write at install time;
|
|
31
|
+
// this covers installs where postinstall didn't run cleanly (pnpm with
|
|
32
|
+
// strict store layouts, --ignore-scripts, cached CI installs, etc).
|
|
33
|
+
config = withExpoVersion(config);
|
|
34
|
+
|
|
28
35
|
// Apply platform specific modifications
|
|
29
36
|
config = withCIOIos(config, props.config, props.ios, props.location);
|
|
30
37
|
config = withCIOAndroid(config, props.config, props.android, props.location);
|
|
@@ -1,4 +1,5 @@
|
|
|
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,
|
|
@@ -7,6 +8,7 @@ import type {
|
|
|
7
8
|
} from '../types/cio-types';
|
|
8
9
|
import { mergeConfigWithEnvValues } from '../utils/config';
|
|
9
10
|
import { logger } from '../utils/logger';
|
|
11
|
+
import { validatePushNotificationOptions } from '../utils/validation';
|
|
10
12
|
import { isExpoVersion53OrHigher } from './utils';
|
|
11
13
|
import { withAppDelegateModifications } from './withAppDelegateModifications';
|
|
12
14
|
import { withCIOIosSwift } from './withCIOIosSwift';
|
|
@@ -25,6 +27,7 @@ export function withCIOIos(
|
|
|
25
27
|
const locationEnabled = location?.enabled === true;
|
|
26
28
|
|
|
27
29
|
if (platformConfig?.pushNotification) {
|
|
30
|
+
validatePushNotificationOptions(platformConfig.pushNotification);
|
|
28
31
|
if (isSwiftProject) {
|
|
29
32
|
config = withCIOIosSwift(config, sdkConfig, platformConfig, location);
|
|
30
33
|
} else {
|
|
@@ -44,6 +47,19 @@ export function withCIOIos(
|
|
|
44
47
|
},
|
|
45
48
|
});
|
|
46
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
|
+
}
|
|
47
63
|
} else if (sdkConfig && isSwiftProject) {
|
|
48
64
|
config = withCIOIosSwift(config, sdkConfig, platformConfig, location);
|
|
49
65
|
if (locationEnabled) {
|
|
@@ -162,6 +162,16 @@ const copyAndConfigurePushAppDelegateHandler = ({
|
|
|
162
162
|
showPushAppInForeground.toString()
|
|
163
163
|
);
|
|
164
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
|
+
|
|
165
175
|
// Add auto initialization if sdkConfig is provided
|
|
166
176
|
if (sdkConfig) {
|
|
167
177
|
// Also copy CustomerIOSDKInitializer.swift for auto-initialization
|
|
@@ -490,34 +500,47 @@ const addDidFailToRegisterForRemoteNotificationsWithError = (
|
|
|
490
500
|
|
|
491
501
|
/**
|
|
492
502
|
* Add deep link handling for killed state
|
|
493
|
-
*
|
|
494
|
-
*
|
|
503
|
+
*
|
|
504
|
+
* On modern Expo Swift templates, RN is bootstrapped by `factory.startReactNative(...)`
|
|
505
|
+
* inside an `#if os(iOS) || os(tvOS)` guard, *before* the trailing `return super.application(...)`.
|
|
506
|
+
* The deep-link block must run before that call so `modifiedLaunchOptions` flows into RN's
|
|
507
|
+
* initial launchOptions; otherwise the workaround is a no-op.
|
|
508
|
+
*
|
|
509
|
+
* For older templates (no `factory.startReactNative` — `super.application(...)` is what
|
|
510
|
+
* starts RN), the snippet is injected before the return statement as before.
|
|
495
511
|
*/
|
|
496
512
|
const addHandleDeeplinkInKilledState = (content: string): string => {
|
|
497
|
-
// Check if deep link code snippet is already present
|
|
498
513
|
const deepLinkMarker = 'Deep link workaround for app killed state start';
|
|
499
514
|
if (content.includes(deepLinkMarker)) {
|
|
500
515
|
return content;
|
|
501
516
|
}
|
|
502
517
|
|
|
503
|
-
// Find the return statement with launchOptions
|
|
504
518
|
const returnStatementRegex =
|
|
505
519
|
/return\s+super\.application\s*\(\s*application\s*,\s*didFinishLaunchingWithOptions\s*:\s*launchOptions\s*\)/;
|
|
506
|
-
const
|
|
520
|
+
const modifiedReturnStatement =
|
|
521
|
+
'return super.application(application, didFinishLaunchingWithOptions: modifiedLaunchOptions)';
|
|
507
522
|
|
|
508
|
-
|
|
523
|
+
const factoryStartRegex =
|
|
524
|
+
/(\s*)#if\s+os\(iOS\)\s*\|\|\s*os\(tvOS\)([\s\S]*?factory\.startReactNative\s*\([\s\S]*?launchOptions:\s*)launchOptions(\s*\)[\s\S]*?#endif)/;
|
|
525
|
+
|
|
526
|
+
if (factoryStartRegex.test(content)) {
|
|
527
|
+
let result = content.replace(
|
|
528
|
+
factoryStartRegex,
|
|
529
|
+
`\n${CIO_CONFIGUREDEEPLINK_KILLEDSTATE_SWIFT_SNIPPET}\n#if os(iOS) || os(tvOS)$2modifiedLaunchOptions$3`
|
|
530
|
+
);
|
|
531
|
+
if (returnStatementRegex.test(result)) {
|
|
532
|
+
result = result.replace(returnStatementRegex, modifiedReturnStatement);
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!returnStatementRegex.test(content)) {
|
|
509
538
|
logger.warn('Could not find return statement with launchOptions');
|
|
510
539
|
return content;
|
|
511
540
|
}
|
|
512
|
-
|
|
513
|
-
// Create the replacement code with deep link handling and modified return statement
|
|
514
|
-
const modifiedReturnStatement =
|
|
515
|
-
'return super.application(application, didFinishLaunchingWithOptions: modifiedLaunchOptions)';
|
|
516
541
|
const replacementCode =
|
|
517
542
|
CIO_CONFIGUREDEEPLINK_KILLEDSTATE_SWIFT_SNIPPET +
|
|
518
543
|
'\n\n ' +
|
|
519
544
|
modifiedReturnStatement;
|
|
520
|
-
|
|
521
|
-
// Replace the return statement with deep link handling code and modified return statement
|
|
522
545
|
return content.replace(returnStatementRegex, replacementCode);
|
|
523
546
|
};
|
|
@@ -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
|
-
|
|
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,32 +1,90 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const RN_SDK_PACKAGE = 'customerio-reactnative';
|
|
5
|
+
|
|
6
|
+
// Locate customerio-reactnative/package.json from a postinstall context.
|
|
7
|
+
//
|
|
8
|
+
// `INIT_CWD` is set by npm, pnpm, and yarn during lifecycle scripts to point
|
|
9
|
+
// at the consumer's project root. That makes it the most reliable starting
|
|
10
|
+
// point — the plugin's own __dirname can be deep inside `.pnpm/...` under pnpm.
|
|
11
|
+
//
|
|
12
|
+
// We probe `${INIT_CWD}/node_modules/customerio-reactnative/package.json` first
|
|
13
|
+
// so we agree with the symlinked layout React Native autolinking expects, then
|
|
14
|
+
// fall back to resolve-from from the consumer root, then to the legacy
|
|
15
|
+
// __dirname-relative walk-up for environments where INIT_CWD is missing.
|
|
16
|
+
function findRNSDKPackageJson() {
|
|
17
|
+
const candidates = [];
|
|
18
|
+
|
|
19
|
+
if (process.env.INIT_CWD) {
|
|
20
|
+
candidates.push(
|
|
21
|
+
path.join(process.env.INIT_CWD, 'node_modules', RN_SDK_PACKAGE, 'package.json')
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Legacy flat-npm layout fallback: plugin lives at
|
|
26
|
+
// <consumer>/node_modules/customerio-expo-plugin/plugin/src and the SDK at
|
|
27
|
+
// <consumer>/node_modules/customerio-reactnative.
|
|
28
|
+
candidates.push(path.join(__dirname, '..', '..', '..', RN_SDK_PACKAGE, 'package.json'));
|
|
29
|
+
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
if (fs.existsSync(candidate)) {
|
|
32
|
+
return candidate;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Final fallback: resolve-from from INIT_CWD. This walks up node_modules
|
|
37
|
+
// and handles yarn classic workspaces where the dep is hoisted.
|
|
38
|
+
if (process.env.INIT_CWD) {
|
|
39
|
+
try {
|
|
40
|
+
const resolveFrom = require('resolve-from');
|
|
41
|
+
const resolved = resolveFrom.silent(
|
|
42
|
+
process.env.INIT_CWD,
|
|
43
|
+
`${RN_SDK_PACKAGE}/package.json`
|
|
44
|
+
);
|
|
45
|
+
if (resolved) return resolved;
|
|
46
|
+
} catch (_) {
|
|
47
|
+
// resolve-from missing or unable to resolve — fall through to null.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
2
53
|
|
|
3
54
|
function runPostInstall() {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const expoPackageJsonFile = `${__dirname}/../../package.json`;
|
|
55
|
+
const expoPackageJsonFile = path.join(__dirname, '..', '..', 'package.json');
|
|
56
|
+
|
|
7
57
|
try {
|
|
8
|
-
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
58
|
+
const reactNativePackageJsonFile = findRNSDKPackageJson();
|
|
59
|
+
if (!reactNativePackageJsonFile) {
|
|
60
|
+
// Not necessarily an error: the plugin may be installed without the RN
|
|
61
|
+
// SDK (e.g., during a tooling-only install). The prebuild-time write
|
|
62
|
+
// covers the common case anyway.
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
15
65
|
|
|
16
|
-
|
|
17
|
-
|
|
66
|
+
const expoPackageJson = require(expoPackageJsonFile);
|
|
67
|
+
const reactNativePackage = JSON.parse(
|
|
68
|
+
fs.readFileSync(reactNativePackageJsonFile, 'utf8')
|
|
69
|
+
);
|
|
18
70
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
JSON.stringify(reactNativePackage, null, 2)
|
|
22
|
-
);
|
|
71
|
+
if (reactNativePackage.expoVersion === expoPackageJson.version) {
|
|
72
|
+
return;
|
|
23
73
|
}
|
|
74
|
+
|
|
75
|
+
reactNativePackage.expoVersion = expoPackageJson.version;
|
|
76
|
+
fs.writeFileSync(
|
|
77
|
+
reactNativePackageJsonFile,
|
|
78
|
+
JSON.stringify(reactNativePackage, null, 2)
|
|
79
|
+
);
|
|
24
80
|
} catch (error) {
|
|
25
81
|
console.warn(
|
|
26
|
-
'
|
|
82
|
+
'customerio-expo-plugin postinstall: failed to write expoVersion into customerio-reactnative/package.json. ' +
|
|
83
|
+
'The expo prebuild step will retry this. Original error:',
|
|
27
84
|
error
|
|
28
85
|
);
|
|
29
86
|
}
|
|
30
87
|
}
|
|
31
88
|
|
|
32
89
|
exports.runPostInstall = runPostInstall;
|
|
90
|
+
exports.findRNSDKPackageJson = findRNSDKPackageJson;
|
|
@@ -171,4 +171,12 @@ export type CustomerIOPluginPushNotificationOptions = {
|
|
|
171
171
|
* Optional if `config` is provided at the top level.
|
|
172
172
|
*/
|
|
173
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;
|
|
174
182
|
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const RN_SDK_PACKAGE = 'customerio-reactnative';
|
|
5
|
+
const REACT_NATIVE_PACKAGE = 'react-native';
|
|
6
|
+
|
|
7
|
+
export type ResolvedRNSDK = {
|
|
8
|
+
// Absolute path to the package directory.
|
|
9
|
+
packageDir: string;
|
|
10
|
+
// Absolute path to package.json inside that directory.
|
|
11
|
+
packageJsonPath: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Reads the installed react-native package's version starting from `fromDir`.
|
|
15
|
+
// Used to decide which autolinking flavor will resolve customerio-reactnative
|
|
16
|
+
// at pod install time, so our :path agrees with what autolinking emits:
|
|
17
|
+
// - RN <0.80 ships @react-native-community/cli, which uses a lexical
|
|
18
|
+
// resolution (no realpath following).
|
|
19
|
+
// - RN >=0.80 routes pod autolinking through expo-modules-autolinking,
|
|
20
|
+
// which realpaths.
|
|
21
|
+
// Returns null if react-native cannot be located or its package.json cannot
|
|
22
|
+
// be read; callers should default to the modern (realpath) behavior in that
|
|
23
|
+
// case since it has been the working path for the last several Expo SDKs.
|
|
24
|
+
export function tryReadRNVersion(fromDir: string): string | null {
|
|
25
|
+
try {
|
|
26
|
+
const direct = path.join(
|
|
27
|
+
fromDir,
|
|
28
|
+
'node_modules',
|
|
29
|
+
REACT_NATIVE_PACKAGE,
|
|
30
|
+
'package.json'
|
|
31
|
+
);
|
|
32
|
+
if (fs.existsSync(direct)) {
|
|
33
|
+
return readVersion(direct);
|
|
34
|
+
}
|
|
35
|
+
const resolveFrom = require('resolve-from');
|
|
36
|
+
const fallback = resolveFrom.silent(
|
|
37
|
+
fromDir,
|
|
38
|
+
`${REACT_NATIVE_PACKAGE}/package.json`
|
|
39
|
+
);
|
|
40
|
+
if (fallback) {
|
|
41
|
+
return readVersion(fallback);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Fall through to null.
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readVersion(pkgJsonPath: string): string | null {
|
|
50
|
+
try {
|
|
51
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
52
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Resolves the customerio-reactnative SDK location starting from `fromDir`.
|
|
59
|
+
//
|
|
60
|
+
// Probe-then-fallback so the result agrees with React Native autolinking
|
|
61
|
+
// across npm flat, pnpm, and yarn-workspace layouts:
|
|
62
|
+
// 1. `fromDir/node_modules/customerio-reactnative/package.json` — preferred.
|
|
63
|
+
// Works for npm flat, pnpm (the symlinked path; we never realpath it),
|
|
64
|
+
// and yarn workspaces with leaf node_modules. Matches what RN
|
|
65
|
+
// autolinking emits for its pod entry, so CocoaPods sees one
|
|
66
|
+
// consistent :path.
|
|
67
|
+
// 2. `resolve-from` walking up from `fromDir` — only used when (1) misses.
|
|
68
|
+
// Handles yarn classic workspaces where the dep is hoisted to a parent
|
|
69
|
+
// node_modules. yarn classic has no symlinks, so the realpath is fine.
|
|
70
|
+
//
|
|
71
|
+
// Returns null if neither finds the package, including the case where
|
|
72
|
+
// `resolve-from` itself can't be required (mirrors tryReadRNVersion's
|
|
73
|
+
// graceful-failure shape so callers can rely on the documented contract).
|
|
74
|
+
export function tryResolveRNSDK(fromDir: string): ResolvedRNSDK | null {
|
|
75
|
+
try {
|
|
76
|
+
const directPkgJson = path.join(
|
|
77
|
+
fromDir,
|
|
78
|
+
'node_modules',
|
|
79
|
+
RN_SDK_PACKAGE,
|
|
80
|
+
'package.json'
|
|
81
|
+
);
|
|
82
|
+
if (fs.existsSync(directPkgJson)) {
|
|
83
|
+
return {
|
|
84
|
+
packageDir: path.dirname(directPkgJson),
|
|
85
|
+
packageJsonPath: directPkgJson,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const resolveFrom = require('resolve-from');
|
|
90
|
+
const fallbackPkgJson = resolveFrom.silent(
|
|
91
|
+
fromDir,
|
|
92
|
+
`${RN_SDK_PACKAGE}/package.json`
|
|
93
|
+
);
|
|
94
|
+
if (fallbackPkgJson) {
|
|
95
|
+
return {
|
|
96
|
+
packageDir: path.dirname(fallbackPkgJson),
|
|
97
|
+
packageJsonPath: fallbackPkgJson,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Fall through to null. resolveRNSDK() turns this into a clear
|
|
102
|
+
// "customerio-reactnative was not found" error for the caller.
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Same as tryResolveRNSDK but throws a clear error when the package is missing.
|
|
109
|
+
export function resolveRNSDK(fromDir: string): ResolvedRNSDK {
|
|
110
|
+
const resolved = tryResolveRNSDK(fromDir);
|
|
111
|
+
if (!resolved) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${RN_SDK_PACKAGE} was not found relative to ${fromDir}. ` +
|
|
114
|
+
`Ensure it is installed in your project (or in a parent workspace's node_modules).`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return resolved;
|
|
118
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NativeSDKConfig, RichPushConfig } from '../types/cio-types';
|
|
1
|
+
import type { CustomerIOPluginPushNotificationOptions, NativeSDKConfig, RichPushConfig } from '../types/cio-types';
|
|
2
2
|
import { logger } from './logger';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -109,8 +109,25 @@ function validateRichPushConfig(config: RichPushConfig | undefined): boolean {
|
|
|
109
109
|
return isValid;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function validatePushNotificationOptions(options: CustomerIOPluginPushNotificationOptions): boolean {
|
|
113
|
+
const context = 'PushNotification';
|
|
114
|
+
|
|
115
|
+
let isValid = true;
|
|
116
|
+
|
|
117
|
+
const appGroupId = options.appGroupId;
|
|
118
|
+
if (appGroupId !== undefined) {
|
|
119
|
+
isValid = validateString(appGroupId, 'appGroupId', context) && isValid;
|
|
120
|
+
if (isValid && !appGroupId.startsWith('group.')) {
|
|
121
|
+
logger.warn(`${context}: appGroupId "${appGroupId}" does not start with "group." — ensure this matches your Apple App Group entitlement.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return isValid;
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
export {
|
|
113
129
|
validateNativeSDKConfig,
|
|
130
|
+
validatePushNotificationOptions,
|
|
114
131
|
validateRequired,
|
|
115
132
|
validateRichPushConfig,
|
|
116
133
|
validateString
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import type { ConfigPlugin } from '@expo/config-plugins';
|
|
3
|
+
|
|
4
|
+
import { logger } from './logger';
|
|
5
|
+
import { getPluginVersion } from './plugin';
|
|
6
|
+
import { tryResolveRNSDK } from './resolveRNSDK';
|
|
7
|
+
|
|
8
|
+
// Writes the plugin's version into customerio-reactnative/package.json under
|
|
9
|
+
// the `expoVersion` key. The RN SDK reads this at runtime (customerio-cdp.ts)
|
|
10
|
+
// to set its User-Agent `packageSource` to "Expo" instead of "ReactNative".
|
|
11
|
+
//
|
|
12
|
+
// We rely on the postinstall hook (postInstallHelper.js) for the same write
|
|
13
|
+
// at install time, but call this from the plugin entry as a fallback for
|
|
14
|
+
// installs where postinstall does not run cleanly — most notably pnpm
|
|
15
|
+
// monorepos and any CI that uses --ignore-scripts.
|
|
16
|
+
//
|
|
17
|
+
// The write is idempotent: we no-op when the value is already correct.
|
|
18
|
+
export function writeExpoVersion(projectRoot: string): void {
|
|
19
|
+
let resolved;
|
|
20
|
+
try {
|
|
21
|
+
resolved = tryResolveRNSDK(projectRoot);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.warn(
|
|
24
|
+
`Could not locate customerio-reactnative to write expoVersion. ` +
|
|
25
|
+
`User-Agent attribution may be incorrect. Original error: ${error}`
|
|
26
|
+
);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!resolved) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const pluginVersion = getPluginVersion();
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(resolved.packageJsonPath, 'utf8'));
|
|
37
|
+
if (pkg.expoVersion === pluginVersion) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
pkg.expoVersion = pluginVersion;
|
|
41
|
+
fs.writeFileSync(
|
|
42
|
+
resolved.packageJsonPath,
|
|
43
|
+
JSON.stringify(pkg, null, 2)
|
|
44
|
+
);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.warn(
|
|
47
|
+
`Failed to write expoVersion into ${resolved.packageJsonPath}. ` +
|
|
48
|
+
`User-Agent attribution may be incorrect. Original error: ${error}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const withExpoVersion: ConfigPlugin = (config) => {
|
|
54
|
+
// _internal.projectRoot is set by Expo when running through prebuild;
|
|
55
|
+
// fall back to process.cwd() for any path that calls the plugin in
|
|
56
|
+
// a non-prebuild context.
|
|
57
|
+
const projectRoot =
|
|
58
|
+
(config as unknown as { _internal?: { projectRoot?: string } })._internal?.projectRoot ||
|
|
59
|
+
process.cwd();
|
|
60
|
+
writeExpoVersion(projectRoot);
|
|
61
|
+
return config;
|
|
62
|
+
};
|