customerio-expo-plugin 3.3.0 → 3.5.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 -1
- package/plugin/lib/commonjs/android/withAndroidManifestUpdates.js +64 -59
- package/plugin/lib/commonjs/android/withAndroidManifestUpdates.js.map +1 -1
- package/plugin/lib/commonjs/android/withAppGoogleServices.js +10 -7
- package/plugin/lib/commonjs/android/withAppGoogleServices.js.map +1 -1
- package/plugin/lib/commonjs/android/withGoogleServicesJSON.js +18 -21
- package/plugin/lib/commonjs/android/withGoogleServicesJSON.js.map +1 -1
- package/plugin/lib/commonjs/android/withLocationGradleProperties.js +16 -12
- package/plugin/lib/commonjs/android/withLocationGradleProperties.js.map +1 -1
- package/plugin/lib/commonjs/android/withMainApplicationModifications.js +19 -12
- package/plugin/lib/commonjs/android/withMainApplicationModifications.js.map +1 -1
- package/plugin/lib/commonjs/android/withNotificationChannelMetadata.js +2 -1
- package/plugin/lib/commonjs/android/withNotificationChannelMetadata.js.map +1 -1
- package/plugin/lib/commonjs/android/withProjectBuildGradle.js +29 -25
- package/plugin/lib/commonjs/android/withProjectBuildGradle.js.map +1 -1
- package/plugin/lib/commonjs/android/withProjectGoogleServices.js +9 -5
- package/plugin/lib/commonjs/android/withProjectGoogleServices.js.map +1 -1
- 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/utils/injectCIOPodfileCode.js +76 -31
- 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/withAppDelegateModifications.js +47 -33
- package/plugin/lib/commonjs/ios/withAppDelegateModifications.js.map +1 -1
- package/plugin/lib/commonjs/ios/withCIOIosSwift.js +44 -54
- package/plugin/lib/commonjs/ios/withCIOIosSwift.js.map +1 -1
- package/plugin/lib/commonjs/ios/withGoogleServicesJsonFile.js +46 -30
- package/plugin/lib/commonjs/ios/withGoogleServicesJsonFile.js.map +1 -1
- package/plugin/lib/commonjs/ios/withNotificationsXcodeProject.js +192 -122
- 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/utils/resolveRNSDK.js +97 -0
- package/plugin/lib/commonjs/utils/resolveRNSDK.js.map +1 -0
- package/plugin/lib/commonjs/utils/writeExpoVersion.js +56 -0
- package/plugin/lib/commonjs/utils/writeExpoVersion.js.map +1 -0
- package/plugin/lib/module/android/withAndroidManifestUpdates.js +61 -58
- package/plugin/lib/module/android/withAndroidManifestUpdates.js.map +1 -1
- package/plugin/lib/module/android/withAppGoogleServices.js +9 -7
- package/plugin/lib/module/android/withAppGoogleServices.js.map +1 -1
- package/plugin/lib/module/android/withGoogleServicesJSON.js +17 -21
- package/plugin/lib/module/android/withGoogleServicesJSON.js.map +1 -1
- package/plugin/lib/module/android/withLocationGradleProperties.js +15 -12
- package/plugin/lib/module/android/withLocationGradleProperties.js.map +1 -1
- package/plugin/lib/module/android/withMainApplicationModifications.js +18 -12
- package/plugin/lib/module/android/withMainApplicationModifications.js.map +1 -1
- package/plugin/lib/module/android/withNotificationChannelMetadata.js +1 -1
- package/plugin/lib/module/android/withNotificationChannelMetadata.js.map +1 -1
- package/plugin/lib/module/android/withProjectBuildGradle.js +28 -25
- package/plugin/lib/module/android/withProjectBuildGradle.js.map +1 -1
- package/plugin/lib/module/android/withProjectGoogleServices.js +8 -5
- package/plugin/lib/module/android/withProjectGoogleServices.js.map +1 -1
- 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/utils/injectCIOPodfileCode.js +74 -31
- 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/withAppDelegateModifications.js +45 -33
- package/plugin/lib/module/ios/withAppDelegateModifications.js.map +1 -1
- package/plugin/lib/module/ios/withCIOIosSwift.js +42 -54
- package/plugin/lib/module/ios/withCIOIosSwift.js.map +1 -1
- package/plugin/lib/module/ios/withGoogleServicesJsonFile.js +45 -30
- package/plugin/lib/module/ios/withGoogleServicesJsonFile.js.map +1 -1
- package/plugin/lib/module/ios/withNotificationsXcodeProject.js +187 -122
- 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/utils/resolveRNSDK.js +88 -0
- package/plugin/lib/module/utils/resolveRNSDK.js.map +1 -0
- package/plugin/lib/module/utils/writeExpoVersion.js +48 -0
- package/plugin/lib/module/utils/writeExpoVersion.js.map +1 -0
- package/plugin/lib/typescript/android/withAndroidManifestUpdates.d.ts +2 -0
- package/plugin/lib/typescript/android/withAppGoogleServices.d.ts +1 -0
- package/plugin/lib/typescript/android/withGoogleServicesJSON.d.ts +1 -0
- package/plugin/lib/typescript/android/withLocationGradleProperties.d.ts +2 -0
- package/plugin/lib/typescript/android/withMainApplicationModifications.d.ts +6 -0
- package/plugin/lib/typescript/android/withNotificationChannelMetadata.d.ts +5 -0
- package/plugin/lib/typescript/android/withProjectBuildGradle.d.ts +9 -0
- package/plugin/lib/typescript/android/withProjectGoogleServices.d.ts +1 -0
- package/plugin/lib/typescript/helpers/constants/ios.d.ts +18 -0
- package/plugin/lib/typescript/helpers/utils/injectCIOPodfileCode.d.ts +25 -1
- package/plugin/lib/typescript/ios/withAppDelegateModifications.d.ts +13 -0
- package/plugin/lib/typescript/ios/withCIOIosSwift.d.ts +11 -0
- package/plugin/lib/typescript/ios/withGoogleServicesJsonFile.d.ts +14 -1
- package/plugin/lib/typescript/ios/withNotificationsXcodeProject.d.ts +53 -2
- package/plugin/lib/typescript/utils/resolveRNSDK.d.ts +7 -0
- package/plugin/lib/typescript/utils/writeExpoVersion.d.ts +3 -0
- package/plugin/src/android/withAndroidManifestUpdates.ts +83 -73
- package/plugin/src/android/withAppGoogleServices.ts +13 -11
- package/plugin/src/android/withGoogleServicesJSON.ts +30 -28
- package/plugin/src/android/withLocationGradleProperties.ts +23 -17
- package/plugin/src/android/withMainApplicationModifications.ts +25 -15
- package/plugin/src/android/withNotificationChannelMetadata.ts +1 -1
- package/plugin/src/android/withProjectBuildGradle.ts +37 -27
- package/plugin/src/android/withProjectGoogleServices.ts +14 -9
- package/plugin/src/helpers/constants/ios.ts +87 -8
- package/plugin/src/helpers/utils/injectCIOPodfileCode.ts +97 -50
- package/plugin/src/index.ts +7 -0
- package/plugin/src/ios/withAppDelegateModifications.ts +61 -48
- package/plugin/src/ios/withCIOIosSwift.ts +58 -62
- package/plugin/src/ios/withGoogleServicesJsonFile.ts +66 -48
- package/plugin/src/ios/withNotificationsXcodeProject.ts +257 -207
- package/plugin/src/postInstallHelper.js +75 -17
- package/plugin/src/utils/resolveRNSDK.ts +118 -0
- package/plugin/src/utils/writeExpoVersion.ts +62 -0
|
@@ -1,18 +1,97 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import * as semver from 'semver';
|
|
3
|
+
|
|
1
4
|
const path = require('path');
|
|
2
|
-
|
|
5
|
+
import { resolveRNSDK, tryReadRNVersion } from '../../utils/resolveRNSDK';
|
|
6
|
+
|
|
7
|
+
// Threshold at which React Native pod autolinking moves from
|
|
8
|
+
// @react-native-community/cli (lexical, symlink-preserving) to
|
|
9
|
+
// expo-modules-autolinking (realpath). The two flavors emit different
|
|
10
|
+
// :path strings on pnpm/yarn-symlink layouts, so to keep CocoaPods happy
|
|
11
|
+
// we must match whichever flavor will resolve the same package later.
|
|
12
|
+
const RN_REALPATH_AUTOLINKING_MIN_VERSION = '0.80.0';
|
|
13
|
+
|
|
14
|
+
const PLUGIN_LOG_PREFIX = '[CustomerIO Plugin]';
|
|
15
|
+
|
|
16
|
+
// Always-on so the trail shows up in customer-shared `expo prebuild`
|
|
17
|
+
// output without needing a separate verbose-mode opt-in.
|
|
18
|
+
function pluginLog(message: string): void {
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
console.log(`${PLUGIN_LOG_PREFIX} ${message}`);
|
|
21
|
+
}
|
|
3
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Returns the relative path from the iOS project dir to the installed
|
|
25
|
+
* customerio-reactnative directory, in the exact form React Native pod
|
|
26
|
+
* autolinking will emit for the same package. The two autolinking
|
|
27
|
+
* flavors disagree on path shape under pnpm/yarn symlinks:
|
|
28
|
+
*
|
|
29
|
+
* - RN <0.80 (`@react-native-community/cli`): walks node_modules
|
|
30
|
+
* lexically, preserves symlinks. We keep the symlink path too —
|
|
31
|
+
* `tryResolveRNSDK` already does this without calling realpath.
|
|
32
|
+
*
|
|
33
|
+
* - RN >=0.80 (`expo-modules-autolinking`): realpaths the package
|
|
34
|
+
* via Node, emitting the underlying `.pnpm/...` (or yarn-classic)
|
|
35
|
+
* path. We match by realpath'ing the resolved directory.
|
|
36
|
+
*
|
|
37
|
+
* Decision points are logged so a customer's prebuild output is enough
|
|
38
|
+
* to triage path-resolution issues without a follow-up "set
|
|
39
|
+
* CUSTOMERIO_DEBUG_MODE and rerun" round-trip.
|
|
40
|
+
*/
|
|
4
41
|
export function getRelativePathToRNSDK(iosPath: string) {
|
|
5
|
-
// Root path of the Expo project
|
|
6
42
|
const rootAppPath = path.dirname(iosPath);
|
|
43
|
+
pluginLog(
|
|
44
|
+
`Resolving customerio-reactnative for Podfile (iosPath=${iosPath}, projectRoot=${rootAppPath})`
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const { packageDir } = resolveRNSDK(rootAppPath);
|
|
48
|
+
pluginLog(`customerio-reactnative resolved to: ${packageDir}`);
|
|
7
49
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
50
|
+
const rnVersion = tryReadRNVersion(rootAppPath);
|
|
51
|
+
pluginLog(`Detected react-native version: ${rnVersion ?? 'unknown'}`);
|
|
52
|
+
|
|
53
|
+
const useLexical = shouldUseLexicalPath(rnVersion);
|
|
54
|
+
pluginLog(
|
|
55
|
+
useLexical
|
|
56
|
+
? `RN <${RN_REALPATH_AUTOLINKING_MIN_VERSION} — using lexical/symlink path to match @react-native-community/cli autolinking`
|
|
57
|
+
: `RN >=${RN_REALPATH_AUTOLINKING_MIN_VERSION} or unknown — using realpath to match expo-modules-autolinking`
|
|
12
58
|
);
|
|
13
59
|
|
|
14
|
-
|
|
15
|
-
|
|
60
|
+
let absolutePath: string;
|
|
61
|
+
if (useLexical) {
|
|
62
|
+
absolutePath = packageDir;
|
|
63
|
+
} else {
|
|
64
|
+
try {
|
|
65
|
+
absolutePath = fs.realpathSync(packageDir);
|
|
66
|
+
if (absolutePath !== packageDir) {
|
|
67
|
+
pluginLog(`Realpath differs from resolved dir: ${absolutePath}`);
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
pluginLog(
|
|
71
|
+
`realpathSync failed (${
|
|
72
|
+
err instanceof Error ? err.message : String(err)
|
|
73
|
+
}); falling back to symlink path`
|
|
74
|
+
);
|
|
75
|
+
absolutePath = packageDir;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const relativePath = path.relative(iosPath, absolutePath);
|
|
80
|
+
pluginLog(`Final Podfile :path => '${relativePath}'`);
|
|
81
|
+
return relativePath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function shouldUseLexicalPath(rnVersion: string | null): boolean {
|
|
85
|
+
if (!rnVersion) {
|
|
86
|
+
// Modern Expo (realpath) has been the working path for the last few
|
|
87
|
+
// SDKs, so it's the safer default when RN can't be detected.
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const coerced = semver.valid(rnVersion) || semver.coerce(rnVersion);
|
|
91
|
+
if (!coerced) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return semver.lt(coerced, RN_REALPATH_AUTOLINKING_MIN_VERSION);
|
|
16
95
|
}
|
|
17
96
|
|
|
18
97
|
export const IOS_DEPLOYMENT_TARGET = '13.0';
|
|
@@ -11,92 +11,139 @@ 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
|
+
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,
|
|
38
58
|
iosPath: string,
|
|
39
59
|
isFcmPushProvider: boolean,
|
|
40
60
|
options?: InjectCIOPodfileOptions
|
|
41
|
-
) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const filename = `${iosPath}/Podfile`;
|
|
46
|
-
const podfile = await FileManagement.read(filename);
|
|
47
|
-
const matches = podfile.match(new RegExp(blockStart));
|
|
48
|
-
|
|
49
|
-
if (!matches) {
|
|
50
|
-
// We need to decide what line of code in the Podfile to insert our native code.
|
|
51
|
-
// The "post_install" line is always present in an Expo project Podfile so it's reliable.
|
|
52
|
-
// Find that line in the Podfile and then we will insert our code above that line.
|
|
53
|
-
const lineInPodfileToInjectSnippetBefore = /post_install do \|installer\|/;
|
|
61
|
+
): string {
|
|
62
|
+
if (podfileContent.match(new RegExp(HOST_APP_BLOCK_START))) {
|
|
63
|
+
return podfileContent;
|
|
64
|
+
}
|
|
54
65
|
|
|
55
|
-
|
|
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);
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
${
|
|
72
|
+
const snippetToInjectInPodfile = `
|
|
73
|
+
${HOST_APP_BLOCK_START}
|
|
59
74
|
${podLine}
|
|
60
|
-
${
|
|
75
|
+
${HOST_APP_BLOCK_END}
|
|
61
76
|
`.trim();
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
snippetToInjectInPodfile
|
|
69
|
-
).join('\n')
|
|
70
|
-
);
|
|
71
|
-
} else {
|
|
72
|
-
logger.info('CustomerIO Podfile snippets already exists. Skipping...');
|
|
73
|
-
}
|
|
78
|
+
return injectCodeByRegex(
|
|
79
|
+
podfileContent,
|
|
80
|
+
lineInPodfileToInjectSnippetBefore,
|
|
81
|
+
snippetToInjectInPodfile,
|
|
82
|
+
).join('\n');
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
export async function
|
|
85
|
+
export async function injectCIOPodfileCode(
|
|
77
86
|
iosPath: string,
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
isFcmPushProvider: boolean,
|
|
88
|
+
options?: InjectCIOPodfileOptions
|
|
80
89
|
) {
|
|
81
90
|
const filename = `${iosPath}/Podfile`;
|
|
82
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
|
+
}
|
|
83
99
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
}
|
|
88
115
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
${blockStart}
|
|
116
|
+
const snippetToAppend = `
|
|
117
|
+
${NOTIFICATION_BLOCK_START}
|
|
92
118
|
target 'NotificationService' do
|
|
93
119
|
${useFrameworks === 'static' ? 'use_frameworks! :linkage => :static' : ''}
|
|
94
|
-
pod 'customerio-reactnative-richpush/${isFcmPushProvider ? 'fcm' : 'apn'
|
|
95
|
-
}', :path => '${getRelativePathToRNSDK(iosPath)}'
|
|
120
|
+
pod 'customerio-reactnative-richpush/${isFcmPushProvider ? 'fcm' : 'apn'}', :path => '${getRelativePathToRNSDK(iosPath)}'
|
|
96
121
|
end
|
|
97
|
-
${
|
|
122
|
+
${NOTIFICATION_BLOCK_END}
|
|
98
123
|
`.trim();
|
|
99
124
|
|
|
100
|
-
|
|
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));
|
|
101
148
|
}
|
|
102
149
|
}
|
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);
|
|
@@ -153,9 +153,12 @@ const addFirebaseDelegateForwardDeclarationIfNeeded = (
|
|
|
153
153
|
return stringContents;
|
|
154
154
|
};
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
|
@@ -232,12 +232,19 @@ export const withCIOIosSwift = (
|
|
|
232
232
|
if (props?.pushNotification) {
|
|
233
233
|
// With push notifications: delegate to CioSdkAppDelegateHandler for both push and auto-init
|
|
234
234
|
return withAppDelegate(configOuter, async (config) => {
|
|
235
|
-
|
|
235
|
+
config.modResults.contents = modifyAppDelegateForPushHandler(
|
|
236
|
+
config.modResults.contents,
|
|
237
|
+
props
|
|
238
|
+
);
|
|
239
|
+
return config;
|
|
236
240
|
});
|
|
237
241
|
} else if (sdkConfig) {
|
|
238
242
|
// Without push notifications: directly inject auto initialization into AppDelegate
|
|
239
243
|
return withAppDelegate(configOuter, async (config) => {
|
|
240
|
-
|
|
244
|
+
config.modResults.contents = modifyAppDelegateForNativeSDKInitializer(
|
|
245
|
+
config.modResults.contents
|
|
246
|
+
);
|
|
247
|
+
return config;
|
|
241
248
|
});
|
|
242
249
|
} else {
|
|
243
250
|
return configOuter;
|
|
@@ -245,77 +252,53 @@ export const withCIOIosSwift = (
|
|
|
245
252
|
};
|
|
246
253
|
|
|
247
254
|
/**
|
|
248
|
-
*
|
|
255
|
+
* Pure string transform: produces the Swift AppDelegate contents wired to delegate to
|
|
256
|
+
* `CioSdkAppDelegateHandler` for both push notifications and (when configured) auto-init.
|
|
257
|
+
* Idempotent — returns `contents` unchanged when the handler is already present.
|
|
249
258
|
*/
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
config: any,
|
|
259
|
+
export function modifyAppDelegateForPushHandler(
|
|
260
|
+
contents: string,
|
|
253
261
|
props: CustomerIOPluginOptionsIOS
|
|
254
|
-
|
|
255
|
-
)
|
|
256
|
-
const appDelegateContent = config.modResults.contents;
|
|
257
|
-
|
|
258
|
-
// Check if modifications have already been applied
|
|
259
|
-
if (appDelegateContent.includes(CIO_SDK_APP_DELEGATE_HANDLER_CLASS)) {
|
|
262
|
+
): string {
|
|
263
|
+
if (contents.includes(CIO_SDK_APP_DELEGATE_HANDLER_CLASS)) {
|
|
260
264
|
logger.info(
|
|
261
265
|
'CustomerIO Swift AppDelegate changes already exist. Skipping...'
|
|
262
266
|
);
|
|
263
|
-
return
|
|
267
|
+
return contents;
|
|
264
268
|
}
|
|
265
269
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Modify didFinishLaunchingWithOptions to initialize and call the handler
|
|
270
|
-
modifiedContent = modifyDidFinishLaunchingWithOptions(
|
|
271
|
-
modifiedContent,
|
|
270
|
+
let next = addHandlerPropertyDeclaration(contents);
|
|
271
|
+
next = modifyDidFinishLaunchingWithOptions(
|
|
272
|
+
next,
|
|
272
273
|
` cioSdkHandler.application(application, didFinishLaunchingWithOptions: launchOptions)\n\n `
|
|
273
274
|
);
|
|
275
|
+
next = addDidRegisterForRemoteNotificationsWithDeviceToken(next);
|
|
276
|
+
next = addDidFailToRegisterForRemoteNotificationsWithError(next);
|
|
274
277
|
|
|
275
|
-
// Add didRegisterForRemoteNotificationsWithDeviceToken implementation
|
|
276
|
-
modifiedContent =
|
|
277
|
-
addDidRegisterForRemoteNotificationsWithDeviceToken(modifiedContent);
|
|
278
|
-
|
|
279
|
-
// Add didFailToRegisterForRemoteNotificationsWithError implementation
|
|
280
|
-
modifiedContent =
|
|
281
|
-
addDidFailToRegisterForRemoteNotificationsWithError(modifiedContent);
|
|
282
|
-
|
|
283
|
-
// Add deep link handling for killed state if enabled
|
|
284
278
|
if (props.pushNotification?.handleDeeplinkInKilledState === true) {
|
|
285
|
-
|
|
279
|
+
next = addHandleDeeplinkInKilledState(next);
|
|
286
280
|
}
|
|
287
281
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
};
|
|
282
|
+
return next;
|
|
283
|
+
}
|
|
291
284
|
|
|
292
285
|
/**
|
|
293
|
-
*
|
|
286
|
+
* Pure string transform: injects the auto-init snippet into the Swift AppDelegate's
|
|
287
|
+
* didFinishLaunchingWithOptions for the no-push path. Idempotent.
|
|
294
288
|
*/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
config: any,
|
|
298
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
299
|
-
): any => {
|
|
300
|
-
const appDelegateContent = config.modResults.contents;
|
|
301
|
-
|
|
302
|
-
// Check if modifications have already been applied
|
|
303
|
-
if (appDelegateContent.includes(CIO_NATIVE_SDK_INITIALIZE_CALL)) {
|
|
289
|
+
export function modifyAppDelegateForNativeSDKInitializer(contents: string): string {
|
|
290
|
+
if (contents.includes(CIO_NATIVE_SDK_INITIALIZE_CALL)) {
|
|
304
291
|
logger.info(
|
|
305
292
|
'CustomerIO Swift AppDelegate changes already exist. Skipping...'
|
|
306
293
|
);
|
|
307
|
-
return
|
|
294
|
+
return contents;
|
|
308
295
|
}
|
|
309
296
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
appDelegateContent,
|
|
297
|
+
return modifyDidFinishLaunchingWithOptions(
|
|
298
|
+
contents,
|
|
313
299
|
CIO_NATIVE_SDK_INITIALIZE_SNIPPET,
|
|
314
300
|
);
|
|
315
|
-
|
|
316
|
-
config.modResults.contents = modifiedContent;
|
|
317
|
-
return config;
|
|
318
|
-
};
|
|
301
|
+
}
|
|
319
302
|
|
|
320
303
|
/**
|
|
321
304
|
* Check if a method exists in the AppDelegate content
|
|
@@ -500,34 +483,47 @@ const addDidFailToRegisterForRemoteNotificationsWithError = (
|
|
|
500
483
|
|
|
501
484
|
/**
|
|
502
485
|
* Add deep link handling for killed state
|
|
503
|
-
*
|
|
504
|
-
*
|
|
486
|
+
*
|
|
487
|
+
* On modern Expo Swift templates, RN is bootstrapped by `factory.startReactNative(...)`
|
|
488
|
+
* inside an `#if os(iOS) || os(tvOS)` guard, *before* the trailing `return super.application(...)`.
|
|
489
|
+
* The deep-link block must run before that call so `modifiedLaunchOptions` flows into RN's
|
|
490
|
+
* initial launchOptions; otherwise the workaround is a no-op.
|
|
491
|
+
*
|
|
492
|
+
* For older templates (no `factory.startReactNative` — `super.application(...)` is what
|
|
493
|
+
* starts RN), the snippet is injected before the return statement as before.
|
|
505
494
|
*/
|
|
506
495
|
const addHandleDeeplinkInKilledState = (content: string): string => {
|
|
507
|
-
// Check if deep link code snippet is already present
|
|
508
496
|
const deepLinkMarker = 'Deep link workaround for app killed state start';
|
|
509
497
|
if (content.includes(deepLinkMarker)) {
|
|
510
498
|
return content;
|
|
511
499
|
}
|
|
512
500
|
|
|
513
|
-
// Find the return statement with launchOptions
|
|
514
501
|
const returnStatementRegex =
|
|
515
502
|
/return\s+super\.application\s*\(\s*application\s*,\s*didFinishLaunchingWithOptions\s*:\s*launchOptions\s*\)/;
|
|
516
|
-
const
|
|
503
|
+
const modifiedReturnStatement =
|
|
504
|
+
'return super.application(application, didFinishLaunchingWithOptions: modifiedLaunchOptions)';
|
|
517
505
|
|
|
518
|
-
|
|
506
|
+
const factoryStartRegex =
|
|
507
|
+
/(\s*)#if\s+os\(iOS\)\s*\|\|\s*os\(tvOS\)([\s\S]*?factory\.startReactNative\s*\([\s\S]*?launchOptions:\s*)launchOptions(\s*\)[\s\S]*?#endif)/;
|
|
508
|
+
|
|
509
|
+
if (factoryStartRegex.test(content)) {
|
|
510
|
+
let result = content.replace(
|
|
511
|
+
factoryStartRegex,
|
|
512
|
+
`\n${CIO_CONFIGUREDEEPLINK_KILLEDSTATE_SWIFT_SNIPPET}\n#if os(iOS) || os(tvOS)$2modifiedLaunchOptions$3`
|
|
513
|
+
);
|
|
514
|
+
if (returnStatementRegex.test(result)) {
|
|
515
|
+
result = result.replace(returnStatementRegex, modifiedReturnStatement);
|
|
516
|
+
}
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!returnStatementRegex.test(content)) {
|
|
519
521
|
logger.warn('Could not find return statement with launchOptions');
|
|
520
522
|
return content;
|
|
521
523
|
}
|
|
522
|
-
|
|
523
|
-
// Create the replacement code with deep link handling and modified return statement
|
|
524
|
-
const modifiedReturnStatement =
|
|
525
|
-
'return super.application(application, didFinishLaunchingWithOptions: modifiedLaunchOptions)';
|
|
526
524
|
const replacementCode =
|
|
527
525
|
CIO_CONFIGUREDEEPLINK_KILLEDSTATE_SWIFT_SNIPPET +
|
|
528
526
|
'\n\n ' +
|
|
529
527
|
modifiedReturnStatement;
|
|
530
|
-
|
|
531
|
-
// Replace the return statement with deep link handling code and modified return statement
|
|
532
528
|
return content.replace(returnStatementRegex, replacementCode);
|
|
533
529
|
};
|