customerio-expo-plugin 3.3.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.
Files changed (40) hide show
  1. package/package.json +7 -1
  2. package/plugin/lib/commonjs/helpers/constants/ios.js +76 -8
  3. package/plugin/lib/commonjs/helpers/constants/ios.js.map +1 -1
  4. package/plugin/lib/commonjs/helpers/utils/injectCIOPodfileCode.js +20 -7
  5. package/plugin/lib/commonjs/helpers/utils/injectCIOPodfileCode.js.map +1 -1
  6. package/plugin/lib/commonjs/index.js +7 -0
  7. package/plugin/lib/commonjs/index.js.map +1 -1
  8. package/plugin/lib/commonjs/ios/withCIOIosSwift.js +18 -12
  9. package/plugin/lib/commonjs/ios/withCIOIosSwift.js.map +1 -1
  10. package/plugin/lib/commonjs/postInstallHelper.js +58 -11
  11. package/plugin/lib/commonjs/postInstallHelper.js.map +1 -1
  12. package/plugin/lib/commonjs/utils/resolveRNSDK.js +97 -0
  13. package/plugin/lib/commonjs/utils/resolveRNSDK.js.map +1 -0
  14. package/plugin/lib/commonjs/utils/writeExpoVersion.js +56 -0
  15. package/plugin/lib/commonjs/utils/writeExpoVersion.js.map +1 -0
  16. package/plugin/lib/module/helpers/constants/ios.js +75 -8
  17. package/plugin/lib/module/helpers/constants/ios.js.map +1 -1
  18. package/plugin/lib/module/helpers/utils/injectCIOPodfileCode.js +20 -7
  19. package/plugin/lib/module/helpers/utils/injectCIOPodfileCode.js.map +1 -1
  20. package/plugin/lib/module/index.js +7 -0
  21. package/plugin/lib/module/index.js.map +1 -1
  22. package/plugin/lib/module/ios/withCIOIosSwift.js +18 -12
  23. package/plugin/lib/module/ios/withCIOIosSwift.js.map +1 -1
  24. package/plugin/lib/module/postInstallHelper.js +58 -11
  25. package/plugin/lib/module/postInstallHelper.js.map +1 -1
  26. package/plugin/lib/module/utils/resolveRNSDK.js +88 -0
  27. package/plugin/lib/module/utils/resolveRNSDK.js.map +1 -0
  28. package/plugin/lib/module/utils/writeExpoVersion.js +48 -0
  29. package/plugin/lib/module/utils/writeExpoVersion.js.map +1 -0
  30. package/plugin/lib/typescript/helpers/constants/ios.d.ts +18 -0
  31. package/plugin/lib/typescript/helpers/utils/injectCIOPodfileCode.d.ts +11 -1
  32. package/plugin/lib/typescript/utils/resolveRNSDK.d.ts +7 -0
  33. package/plugin/lib/typescript/utils/writeExpoVersion.d.ts +3 -0
  34. package/plugin/src/helpers/constants/ios.ts +87 -8
  35. package/plugin/src/helpers/utils/injectCIOPodfileCode.ts +22 -10
  36. package/plugin/src/index.ts +7 -0
  37. package/plugin/src/ios/withCIOIosSwift.ts +25 -12
  38. package/plugin/src/postInstallHelper.js +75 -17
  39. package/plugin/src/utils/resolveRNSDK.ts +118 -0
  40. 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
- const resolveFrom = require('resolve-from');
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
- // Path of the cio RN package.json file. Example: test-app/node_modules/customerio-reactnative/package.json
9
- const pluginPackageJsonPath = resolveFrom.silent(
10
- rootAppPath,
11
- `customerio-reactnative/package.json`
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
- // Example: ../node_modules/customerio-reactnative
15
- return path.relative(iosPath, path.dirname(pluginPackageJsonPath));
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,27 +11,35 @@ export type InjectCIOPodfileOptions = {
11
11
  hasPush?: boolean;
12
12
  };
13
13
 
14
- /** Builds the host app pod line for the Podfile (single subspec or :subspecs with location). Exported for tests. */
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 path = getRelativePathToRNSDK(iosPath);
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 => '${path}'`;
36
+ return `pod 'customerio-reactnative/${subspec}', :path => '${resolvedPath}'`;
27
37
  }
28
-
29
38
  if (!hasPush) {
30
- return `pod 'customerio-reactnative', :subspecs => ['location'], :path => '${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 => '${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
- ${useFrameworks === 'static' ? 'use_frameworks! :linkage => :static' : ''}
94
- pod 'customerio-reactnative-richpush/${isFcmPushProvider ? 'fcm' : 'apn'
95
- }', :path => '${getRelativePathToRNSDK(iosPath)}'
106
+ ${useFrameworksLine}
107
+ pod 'customerio-reactnative-richpush/${subspec}', :path => '${resolvedPath}'
96
108
  end
97
109
  ${blockEnd}
98
110
  `.trim();
@@ -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);
@@ -500,34 +500,47 @@ const addDidFailToRegisterForRemoteNotificationsWithError = (
500
500
 
501
501
  /**
502
502
  * Add deep link handling for killed state
503
- * This replaces the return statement with deep link handling code
504
- * and a modified return statement that uses modifiedLaunchOptions
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.
505
511
  */
506
512
  const addHandleDeeplinkInKilledState = (content: string): string => {
507
- // Check if deep link code snippet is already present
508
513
  const deepLinkMarker = 'Deep link workaround for app killed state start';
509
514
  if (content.includes(deepLinkMarker)) {
510
515
  return content;
511
516
  }
512
517
 
513
- // Find the return statement with launchOptions
514
518
  const returnStatementRegex =
515
519
  /return\s+super\.application\s*\(\s*application\s*,\s*didFinishLaunchingWithOptions\s*:\s*launchOptions\s*\)/;
516
- const returnStatementMatch = content.match(returnStatementRegex);
520
+ const modifiedReturnStatement =
521
+ 'return super.application(application, didFinishLaunchingWithOptions: modifiedLaunchOptions)';
517
522
 
518
- if (!returnStatementMatch) {
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)) {
519
538
  logger.warn('Could not find return statement with launchOptions');
520
539
  return content;
521
540
  }
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
541
  const replacementCode =
527
542
  CIO_CONFIGUREDEEPLINK_KILLEDSTATE_SWIFT_SNIPPET +
528
543
  '\n\n ' +
529
544
  modifiedReturnStatement;
530
-
531
- // Replace the return statement with deep link handling code and modified return statement
532
545
  return content.replace(returnStatementRegex, replacementCode);
533
546
  };
@@ -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
- // react native SDK package.json path
5
- const reactNativePackageJsonFile = `${__dirname}/../../../customerio-reactnative/package.json`;
6
- const expoPackageJsonFile = `${__dirname}/../../package.json`;
55
+ const expoPackageJsonFile = path.join(__dirname, '..', '..', 'package.json');
56
+
7
57
  try {
8
- // if react native SDK is installed
9
- if (fs.existsSync(reactNativePackageJsonFile)) {
10
- const reactNativePackageJson = fs.readFileSync(
11
- reactNativePackageJsonFile,
12
- 'utf8'
13
- );
14
- const expoPackageJson = require(expoPackageJsonFile);
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
- const reactNativePackage = JSON.parse(reactNativePackageJson);
17
- reactNativePackage.expoVersion = expoPackageJson.version;
66
+ const expoPackageJson = require(expoPackageJsonFile);
67
+ const reactNativePackage = JSON.parse(
68
+ fs.readFileSync(reactNativePackageJsonFile, 'utf8')
69
+ );
18
70
 
19
- fs.writeFileSync(
20
- reactNativePackageJsonFile,
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
- 'Unable to find customerio-reactnative package.json file. Please make sure you have installed the customerio-reactnative package.',
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;
@@ -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
+ }
@@ -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
+ };