@amplytools/react-native-amply-sdk 0.1.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 (88) hide show
  1. package/LICENSE +178 -0
  2. package/README.md +714 -0
  3. package/android/build.gradle +90 -0
  4. package/android/consumer-rules.pro +1 -0
  5. package/android/gradle.properties +3 -0
  6. package/android/settings.gradle +9 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +384 -0
  9. package/android/src/main/java/tools/amply/sdk/reactnative/AmplyPackage.kt +39 -0
  10. package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +30 -0
  11. package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +296 -0
  12. package/android/src/main/java/tools/amply/sdk/reactnative/model/AmplyInitializationOptions.kt +10 -0
  13. package/android/src/main/java/tools/amply/sdk/reactnative/model/DataSetType.kt +42 -0
  14. package/android/src/main/java/tools/amply/sdk/reactnative/model/DataSetTypeMapper.kt +38 -0
  15. package/android/src/main/java/tools/amply/sdk/reactnative/model/DeepLinkPayload.kt +8 -0
  16. package/android/src/main/java/tools/amply/sdk/reactnative/model/EventEnvelope.kt +9 -0
  17. package/android/src/main/jni/AmplyTurboModule.cpp +29 -0
  18. package/android/src/main/jni/CMakeLists.txt +76 -0
  19. package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +75 -0
  20. package/android/src/newarch/jni/AmplyReactNative-generated.cpp +77 -0
  21. package/android/src/newarch/jni/AmplyReactNative.h +31 -0
  22. package/android/src/newarch/jni/CMakeLists.txt +40 -0
  23. package/app.plugin.js +1 -0
  24. package/dist/index.js +272 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/index.mjs +234 -0
  27. package/dist/index.mjs.map +1 -0
  28. package/dist/plugin/index.d.ts +6 -0
  29. package/dist/plugin/index.d.ts.map +1 -0
  30. package/dist/plugin/index.js +186 -0
  31. package/dist/plugin/index.js.map +1 -0
  32. package/dist/plugin/index.mjs +169 -0
  33. package/dist/plugin/index.mjs.map +1 -0
  34. package/dist/plugin/src/index.d.ts +6 -0
  35. package/dist/plugin/src/index.d.ts.map +1 -0
  36. package/dist/plugin/src/index.js +3 -0
  37. package/dist/plugin/src/withAmply.d.ts +30 -0
  38. package/dist/plugin/src/withAmply.d.ts.map +1 -0
  39. package/dist/plugin/src/withAmply.js +51 -0
  40. package/dist/plugin/withAmply.d.ts +12 -0
  41. package/dist/plugin/withAmply.d.ts.map +1 -0
  42. package/dist/src/__tests__/index.test.d.ts +2 -0
  43. package/dist/src/__tests__/index.test.d.ts.map +1 -0
  44. package/dist/src/__tests__/index.test.js +70 -0
  45. package/dist/src/hooks/useAmplySystemEvents.d.ts +12 -0
  46. package/dist/src/hooks/useAmplySystemEvents.d.ts.map +1 -0
  47. package/dist/src/hooks/useAmplySystemEvents.js +56 -0
  48. package/dist/src/index.d.ts +32 -0
  49. package/dist/src/index.d.ts.map +1 -0
  50. package/dist/src/index.js +80 -0
  51. package/dist/src/nativeModule.d.ts +5 -0
  52. package/dist/src/nativeModule.d.ts.map +1 -0
  53. package/dist/src/nativeModule.js +48 -0
  54. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +75 -0
  55. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -0
  56. package/dist/src/nativeSpecs/NativeAmplyModule.js +2 -0
  57. package/dist/src/systemEventUtils.d.ts +3 -0
  58. package/dist/src/systemEventUtils.d.ts.map +1 -0
  59. package/dist/src/systemEventUtils.js +30 -0
  60. package/dist/src/systemEvents.d.ts +6 -0
  61. package/dist/src/systemEvents.d.ts.map +1 -0
  62. package/dist/src/systemEvents.js +8 -0
  63. package/dist/tsconfig.tsbuildinfo +1 -0
  64. package/docs/ARCHITECTURE.md +1115 -0
  65. package/expo-module.config.json +11 -0
  66. package/ios/AmplyReactNative.podspec +32 -0
  67. package/ios/README.md +11 -0
  68. package/ios/Sources/AmplyReactNative/AmplyModule.mm +332 -0
  69. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +111 -0
  70. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +152 -0
  71. package/package.json +71 -0
  72. package/plugin/build/index.d.ts +5 -0
  73. package/plugin/build/index.js +8 -0
  74. package/plugin/build/withAmply.d.ts +29 -0
  75. package/plugin/build/withAmply.js +53 -0
  76. package/plugin/src/index.ts +7 -0
  77. package/plugin/src/withAmply.ts +68 -0
  78. package/plugin/tsconfig.json +8 -0
  79. package/plugin/tsconfig.tsbuildinfo +1 -0
  80. package/react-native.config.js +34 -0
  81. package/scripts/codegen.js +212 -0
  82. package/src/__tests__/index.test.ts +92 -0
  83. package/src/hooks/useAmplySystemEvents.ts +75 -0
  84. package/src/index.ts +115 -0
  85. package/src/nativeModule.ts +65 -0
  86. package/src/nativeSpecs/NativeAmplyModule.ts +80 -0
  87. package/src/systemEventUtils.ts +35 -0
  88. package/src/systemEvents.ts +13 -0
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@amplytools/react-native-amply-sdk",
3
+ "version": "0.1.0",
4
+ "description": "React Native SDK for Amply: Mobile SDK to orchestrate in-app experiences and campaigns remotely, without app releases",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/amply/amply-react-native.git"
9
+ },
10
+ "homepage": "https://github.com/amply/amply-react-native",
11
+ "bugs": {
12
+ "url": "https://github.com/amply/amply-react-native/issues"
13
+ },
14
+ "main": "dist/index.js",
15
+ "module": "dist/index.mjs",
16
+ "types": "dist/src/index.d.ts",
17
+ "react-native": "src/index.ts",
18
+ "source": "src/index.ts",
19
+ "files": [
20
+ "android/",
21
+ "!android/.cxx/",
22
+ "!android/build/",
23
+ "!android/.gradle/",
24
+ "ios/",
25
+ "dist/",
26
+ "src/",
27
+ "plugin/",
28
+ "scripts/",
29
+ "react-native.config.js",
30
+ "app.plugin.js",
31
+ "expo-module.config.json",
32
+ "README.md",
33
+ "docs/"
34
+ ],
35
+ "scripts": {
36
+ "build": "expo-module build",
37
+ "build:plugin": "expo-module build plugin",
38
+ "clean": "expo-module clean",
39
+ "test": "expo-module test",
40
+ "prepare": "expo-module prepare",
41
+ "prepublishOnly": "expo-module prepublishOnly"
42
+ },
43
+ "peerDependencies": {
44
+ "expo": ">=54.0.0",
45
+ "react": ">=18.2.0",
46
+ "react-native": ">=0.79.0"
47
+ },
48
+ "devDependencies": {
49
+ "@react-native/codegen": "0.81.4",
50
+ "@react-native/eslint-config": "^0.74.81",
51
+ "@react-native/typescript-config": "0.81.0",
52
+ "@types/jest": "^29.5.11",
53
+ "@types/node": "^25.0.3",
54
+ "@types/react": "^19.1.1",
55
+ "@types/react-native": "^0.73.0",
56
+ "eslint": "^8.56.0",
57
+ "expo": "^54.0.0",
58
+ "expo-module-scripts": "^3.5.0",
59
+ "jest": "^29.7.0",
60
+ "typescript": "^5.4.0"
61
+ },
62
+ "engines": {
63
+ "node": ">=18"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "expo": {
69
+ "plugin": "./app.plugin.js"
70
+ }
71
+ }
@@ -0,0 +1,5 @@
1
+ import type { ConfigPlugin } from '@expo/config-plugins';
2
+ declare const plugin: ConfigPlugin<{
3
+ sdkVersion?: string;
4
+ }>;
5
+ export default plugin;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const withAmply_1 = __importDefault(require("./withAmply"));
7
+ const plugin = (config, props) => (0, withAmply_1.default)(config, props);
8
+ exports.default = plugin;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Expo Config Plugin for Amply React Native SDK
3
+ *
4
+ * The Amply React Native SDK is a TurboModule that uses react-native.config.js
5
+ * for autolinking in Bare React Native. For Expo apps, this plugin ensures
6
+ * AmplyPackage is registered in MainApplication (Android).
7
+ *
8
+ * iOS: AmplySDK is fetched automatically from CocoaPods Trunk via the podspec
9
+ * dependency declaration - no Podfile modifications needed.
10
+ *
11
+ * Architecture:
12
+ * - Bare RN: Uses react-native.config.js autolinking (fully automatic)
13
+ * - Expo: Uses react-native.config.js + Expo config plugin for Android registration
14
+ *
15
+ * Usage:
16
+ * 1. Add to app.json: "@amply/amply-react-native" in plugins array
17
+ * 2. Run: expo prebuild --clean
18
+ * 3. Run: expo start and expo run:android/ios
19
+ */
20
+ import { ConfigPlugin } from '@expo/config-plugins';
21
+ type WithAmplyPluginProps = {};
22
+ /**
23
+ * Expo Config Plugin for Amply React Native SDK
24
+ *
25
+ * Registers AmplyPackage in MainApplication for Expo apps (Android).
26
+ * iOS uses CocoaPods Trunk for AmplySDK - no modifications needed.
27
+ */
28
+ declare const withAmply: ConfigPlugin<WithAmplyPluginProps>;
29
+ export default withAmply;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Expo Config Plugin for Amply React Native SDK
4
+ *
5
+ * The Amply React Native SDK is a TurboModule that uses react-native.config.js
6
+ * for autolinking in Bare React Native. For Expo apps, this plugin ensures
7
+ * AmplyPackage is registered in MainApplication (Android).
8
+ *
9
+ * iOS: AmplySDK is fetched automatically from CocoaPods Trunk via the podspec
10
+ * dependency declaration - no Podfile modifications needed.
11
+ *
12
+ * Architecture:
13
+ * - Bare RN: Uses react-native.config.js autolinking (fully automatic)
14
+ * - Expo: Uses react-native.config.js + Expo config plugin for Android registration
15
+ *
16
+ * Usage:
17
+ * 1. Add to app.json: "@amply/amply-react-native" in plugins array
18
+ * 2. Run: expo prebuild --clean
19
+ * 3. Run: expo start and expo run:android/ios
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ const config_plugins_1 = require("@expo/config-plugins");
23
+ /**
24
+ * Modifies Android MainApplication to register AmplyPackage
25
+ */
26
+ const withAmplyAndroid = (config) => {
27
+ return (0, config_plugins_1.withMainApplication)(config, async (cfg) => {
28
+ let contents = cfg.modResults.contents;
29
+ // Check if AmplyPackage is already imported
30
+ if (!contents.includes('import tools.amply.sdk.reactnative.AmplyPackage')) {
31
+ // Add import after the expo imports
32
+ contents = contents.replace(/(import expo\.modules\.ReactNativeHostWrapper)/, `import tools.amply.sdk.reactnative.AmplyPackage\n$1`);
33
+ }
34
+ // For Kotlin syntax: packages.apply { ... }
35
+ // We need to add the package instance inside the apply block
36
+ if (!contents.includes('add(AmplyPackage())')) {
37
+ contents = contents.replace(/(PackageList\(this\)\.packages\.apply \{[\s\S]*?)(\/\/ add.*?)\n(\s+\})/, `$1$2\n add(AmplyPackage())\n$3`);
38
+ }
39
+ cfg.modResults.contents = contents;
40
+ return cfg;
41
+ });
42
+ };
43
+ /**
44
+ * Expo Config Plugin for Amply React Native SDK
45
+ *
46
+ * Registers AmplyPackage in MainApplication for Expo apps (Android).
47
+ * iOS uses CocoaPods Trunk for AmplySDK - no modifications needed.
48
+ */
49
+ const withAmply = (config) => {
50
+ // Apply Android modifications
51
+ return withAmplyAndroid(config);
52
+ };
53
+ exports.default = withAmply;
@@ -0,0 +1,7 @@
1
+ import type {ConfigPlugin} from '@expo/config-plugins';
2
+ import withAmply from './withAmply';
3
+
4
+ const plugin: ConfigPlugin<{ sdkVersion?: string }> = (config, props) =>
5
+ withAmply(config, props);
6
+
7
+ export default plugin;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Expo Config Plugin for Amply React Native SDK
3
+ *
4
+ * The Amply React Native SDK is a TurboModule that uses react-native.config.js
5
+ * for autolinking in Bare React Native. For Expo apps, this plugin ensures
6
+ * AmplyPackage is registered in MainApplication (Android).
7
+ *
8
+ * iOS: AmplySDK is fetched automatically from CocoaPods Trunk via the podspec
9
+ * dependency declaration - no Podfile modifications needed.
10
+ *
11
+ * Architecture:
12
+ * - Bare RN: Uses react-native.config.js autolinking (fully automatic)
13
+ * - Expo: Uses react-native.config.js + Expo config plugin for Android registration
14
+ *
15
+ * Usage:
16
+ * 1. Add to app.json: "@amply/amply-react-native" in plugins array
17
+ * 2. Run: expo prebuild --clean
18
+ * 3. Run: expo start and expo run:android/ios
19
+ */
20
+
21
+ import { ConfigPlugin, withMainApplication } from '@expo/config-plugins';
22
+
23
+ type WithAmplyPluginProps = {
24
+ // Props for future use
25
+ };
26
+
27
+ /**
28
+ * Modifies Android MainApplication to register AmplyPackage
29
+ */
30
+ const withAmplyAndroid: ConfigPlugin = (config) => {
31
+ return withMainApplication(config, async (cfg) => {
32
+ let contents = cfg.modResults.contents;
33
+
34
+ // Check if AmplyPackage is already imported
35
+ if (!contents.includes('import tools.amply.sdk.reactnative.AmplyPackage')) {
36
+ // Add import after the expo imports
37
+ contents = contents.replace(
38
+ /(import expo\.modules\.ReactNativeHostWrapper)/,
39
+ `import tools.amply.sdk.reactnative.AmplyPackage\n$1`
40
+ );
41
+ }
42
+
43
+ // For Kotlin syntax: packages.apply { ... }
44
+ // We need to add the package instance inside the apply block
45
+ if (!contents.includes('add(AmplyPackage())')) {
46
+ contents = contents.replace(
47
+ /(PackageList\(this\)\.packages\.apply \{[\s\S]*?)(\/\/ add.*?)\n(\s+\})/,
48
+ `$1$2\n add(AmplyPackage())\n$3`
49
+ );
50
+ }
51
+
52
+ cfg.modResults.contents = contents;
53
+ return cfg;
54
+ });
55
+ };
56
+
57
+ /**
58
+ * Expo Config Plugin for Amply React Native SDK
59
+ *
60
+ * Registers AmplyPackage in MainApplication for Expo apps (Android).
61
+ * iOS uses CocoaPods Trunk for AmplySDK - no modifications needed.
62
+ */
63
+ const withAmply: ConfigPlugin<WithAmplyPluginProps> = (config) => {
64
+ // Apply Android modifications
65
+ return withAmplyAndroid(config);
66
+ };
67
+
68
+ export default withAmply;
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "expo-module-scripts/tsconfig.plugin",
3
+ "compilerOptions": {
4
+ "outDir": "build",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["./src"]
8
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/index.ts","./src/withamply.ts"],"version":"5.9.3"}
@@ -0,0 +1,34 @@
1
+ const path = require('path');
2
+
3
+ const codegenSourceDirAndroid = path.join(__dirname, 'android', 'src', 'newarch');
4
+ const codegenSourceDirIos = path.join(__dirname, 'ios', 'Sources', 'AmplyReactNative');
5
+ const androidProjectDir = path.join(__dirname, 'android');
6
+ const androidJniDir = path.join(androidProjectDir, 'src', 'main', 'jni');
7
+
8
+ module.exports = {
9
+ dependency: {
10
+ platforms: {
11
+ android: {
12
+ sourceDir: androidProjectDir,
13
+ packageImportPath: 'import tools.amply.sdk.reactnative.AmplyPackage;',
14
+ packageInstance: 'new AmplyPackage()',
15
+ cmakeListsPath: path.join(androidJniDir, 'CMakeLists.txt'),
16
+ },
17
+ ios: null,
18
+ },
19
+ },
20
+ commands: [],
21
+ assets: [],
22
+ codegenConfig: {
23
+ name: 'AmplyReactNative',
24
+ type: 'modules',
25
+ jsSrcsDir: path.join(__dirname, 'src'),
26
+ android: {
27
+ sourceDir: codegenSourceDirAndroid,
28
+ packageName: 'tools.amply.sdk.reactnative',
29
+ },
30
+ ios: {
31
+ sourceDir: codegenSourceDirIos,
32
+ },
33
+ },
34
+ };
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const {
5
+ combineSchemasInFileList,
6
+ } = require('@react-native/codegen/lib/cli/combine/combine-js-to-schema');
7
+ const RNCodegen = require('@react-native/codegen/lib/generators/RNCodegen');
8
+
9
+ const ROOT = path.resolve(__dirname, '..');
10
+ const configPath = path.join(ROOT, 'codegen.config.json');
11
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
12
+
13
+ /**
14
+ * Clean up stale generated files from previous codegen runs with different libraryName.
15
+ * RN codegen doesn't automatically clean old files when libraryName changes.
16
+ */
17
+ function cleanupStaleGeneratedFiles(outputDir, currentLibraryName) {
18
+ if (!fs.existsSync(outputDir)) {
19
+ return;
20
+ }
21
+
22
+ const files = fs.readdirSync(outputDir);
23
+ const stalePatterns = [
24
+ /-generated\.(cpp|h)$/, // Files ending in -generated.cpp or -generated.h
25
+ /^[^/]+(\.h)$/, // Header files
26
+ ];
27
+
28
+ // Only delete files that don't match current library name
29
+ files.forEach(file => {
30
+ if (file.includes(currentLibraryName)) {
31
+ return; // Keep files with current library name
32
+ }
33
+
34
+ if (stalePatterns.some(pattern => pattern.test(file))) {
35
+ const filePath = path.join(outputDir, file);
36
+ try {
37
+ fs.unlinkSync(filePath);
38
+ } catch (err) {
39
+ // Silently ignore - might be a directory
40
+ }
41
+ }
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Патчим android/src/newarch/jni/CMakeLists.txt после генерации.
47
+ * RN 0.79 в Gradle передаёт в CMake параметр:
48
+ * -DREACT_NATIVE_DIR=/.../node_modules/react-native
49
+ *
50
+ * Мы из него делаем REACT_COMMON_DIR и подрубаем
51
+ * ReactCommon/cmake-utils/react-native-flags.cmake
52
+ * Там как раз и объявлен target_compile_reactnative_options.
53
+ */
54
+ function patchAndroidCMake(cmakePath) {
55
+ if (!fs.existsSync(cmakePath)) {
56
+ return;
57
+ }
58
+
59
+ const original = fs.readFileSync(cmakePath, 'utf-8');
60
+
61
+ // Уже патчили — выходим.
62
+ if (original.includes('react-native-flags.cmake')) {
63
+ return;
64
+ }
65
+
66
+ const insertSnippet = `
67
+ # --- React Native 0.79: wire up compile flags ---
68
+ if(NOT DEFINED REACT_NATIVE_DIR)
69
+ message(FATAL_ERROR "REACT_NATIVE_DIR is not defined; it must point to node_modules/react-native")
70
+ endif()
71
+
72
+ # Gradle (externalNativeBuild) passes REACT_NATIVE_DIR.
73
+ # From it we derive ReactCommon path used by RN CMake utils.
74
+ set(REACT_COMMON_DIR "\${REACT_NATIVE_DIR}/ReactCommon")
75
+
76
+ # This provides target_compile_reactnative_options and imported targets
77
+ # like 'reactnative', 'jsi', 'fbjni', etc.
78
+ include(\${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
79
+ # --- end RN wiring ---
80
+ `.trim();
81
+
82
+ const linesIn = original.split('\n');
83
+ const linesOut = [];
84
+ let inserted = false;
85
+
86
+ for (let i = 0; i < linesIn.length; i++) {
87
+ const line = linesIn[i];
88
+ linesOut.push(line);
89
+
90
+ // Вставляем наш блок сразу после set(CMAKE_VERBOSE_MAKEFILE on)
91
+ if (!inserted && line.match(/set\s*\(\s*CMAKE_VERBOSE_MAKEFILE\s+on\s*\)/)) {
92
+ linesOut.push('');
93
+ linesOut.push(insertSnippet);
94
+ linesOut.push('');
95
+ inserted = true;
96
+ }
97
+ }
98
+
99
+ const updated = linesOut.join('\n');
100
+ fs.writeFileSync(cmakePath, updated);
101
+ }
102
+
103
+ /**
104
+ * Patch the newarch CMakeLists.txt to add missing JSI include directories
105
+ * and ensure proper inclusion of React headers.
106
+ */
107
+ function enhanceNewarchCMake(cmakePath) {
108
+ if (!fs.existsSync(cmakePath)) {
109
+ return;
110
+ }
111
+
112
+ let content = fs.readFileSync(cmakePath, 'utf-8');
113
+
114
+ // Check if we've already added the JSI includes
115
+ if (content.includes('REACT_JSI_DIR')) {
116
+ return;
117
+ }
118
+
119
+ // Additional imports needed for generated TurboModule code
120
+ const jsiEnhancement = `\n# Ensure fbjni is available as imported target
121
+ find_package(fbjni REQUIRED CONFIG)
122
+
123
+ # For OBJECT libraries, we need to explicitly add include directories since
124
+ # target_link_libraries doesn't propagate INTERFACE_INCLUDE_DIRECTORIES for OBJECT libs
125
+ set(REACT_JSI_DIR "\${REACT_COMMON_DIR}/jsi")
126
+ `;
127
+
128
+ // Insert after the react-native-flags.cmake include, before file(GLOB...)
129
+ content = content.replace(
130
+ /# --- end RN wiring ---\n\n\nfile\(GLOB/,
131
+ '# --- end RN wiring ---' + jsiEnhancement + '\nfile(GLOB'
132
+ );
133
+
134
+ // For OBJECT libraries, we need to add JSI include directories explicitly
135
+ // Note: folly and reactnative includes will be added by the parent CMakeLists.txt
136
+ // after targets are created. We only need JSI here since it's a source directory.
137
+ const includePattern = /target_include_directories\(react_codegen_\w+ PUBLIC \. react\/renderer\/components\/\w+\)/g;
138
+ content = content.replace(includePattern, (match) => {
139
+ if (match.includes('${REACT_JSI_DIR}')) {
140
+ return match; // Already has includes
141
+ }
142
+ // Add JSI include path
143
+ return match.replace(')', ` \${REACT_JSI_DIR})`);
144
+ });
145
+
146
+ // Remove target_link_libraries from OBJECT library
147
+ // OBJECT libraries don't need to be linked in the generated CMakeLists
148
+ // The parent CMakeLists will link the main library against the necessary targets
149
+ // and add includes for the OBJECT library separately.
150
+ // This avoids duplicate linking and unresolved symbols in the final link step.
151
+ content = content.replace(
152
+ /target_link_libraries\([^)]+fbjni[^)]+jsi[^)]+reactnative[^)]*\)\n/s,
153
+ ''
154
+ );
155
+
156
+ fs.writeFileSync(cmakePath, content);
157
+ }
158
+
159
+ /**
160
+ * Генерация для одного модуля из codegen.config.json
161
+ */
162
+ function generateModule(name, moduleConfig) {
163
+ const jsSpecFile = path.resolve(ROOT, moduleConfig.jsSpecFile);
164
+ const schema = combineSchemasInFileList([jsSpecFile]);
165
+ const libraryName = moduleConfig.libraryName ?? name;
166
+
167
+ const androidConfig = moduleConfig.android;
168
+ if (androidConfig && androidConfig.sourceDir) {
169
+ const outputDirectory = path.resolve(ROOT, androidConfig.sourceDir);
170
+ const jniDir = path.join(outputDirectory, 'jni');
171
+
172
+ // Clean up stale files before generating
173
+ cleanupStaleGeneratedFiles(jniDir, libraryName);
174
+
175
+ RNCodegen.generate(
176
+ {
177
+ libraryName,
178
+ schema,
179
+ outputDirectory,
180
+ packageName: androidConfig.packageName ?? 'com.facebook.fbreact.specs',
181
+ assumeNonnull: true,
182
+ },
183
+ {generators: ['modulesAndroid']},
184
+ );
185
+
186
+ const cmakePath = path.join(outputDirectory, 'jni', 'CMakeLists.txt');
187
+ patchAndroidCMake(cmakePath);
188
+ enhanceNewarchCMake(cmakePath); // Add JSI and React header configurations
189
+ }
190
+
191
+ const iosConfig = moduleConfig.ios;
192
+ if (iosConfig && iosConfig.sourceDir) {
193
+ RNCodegen.generate(
194
+ {
195
+ libraryName,
196
+ schema,
197
+ outputDirectory: path.resolve(ROOT, iosConfig.sourceDir),
198
+ moduleName: iosConfig.moduleName ?? libraryName,
199
+ assumeNonnull: true,
200
+ },
201
+ {generators: ['modulesIOS']},
202
+ );
203
+ }
204
+ }
205
+
206
+ // Обрабатываем все модули из codegen.config.json
207
+ Object.entries(config.modules).forEach(([name, moduleConfig]) => {
208
+ if (moduleConfig.type !== 'NativeModule') {
209
+ return;
210
+ }
211
+ generateModule(name, moduleConfig);
212
+ });
@@ -0,0 +1,92 @@
1
+ import * as Amply from '../index';
2
+ import {__setNativeModule} from '../nativeModule';
3
+
4
+ const mockNativeModule = {
5
+ initialize: jest.fn(),
6
+ isInitialized: jest.fn(() => true),
7
+ track: jest.fn(),
8
+ getRecentEvents: jest.fn(),
9
+ getDataSetSnapshot: jest.fn(),
10
+ registerDeepLinkListener: jest.fn(),
11
+ addListener: jest.fn(),
12
+ removeListeners: jest.fn(),
13
+ onSystemEvent: jest.fn(),
14
+ onDeepLink: jest.fn(),
15
+ };
16
+
17
+ describe('Amply JS API', () => {
18
+ afterEach(() => {
19
+ jest.clearAllMocks();
20
+ __setNativeModule(null);
21
+ });
22
+
23
+ beforeEach(() => {
24
+ __setNativeModule(mockNativeModule as never);
25
+ });
26
+
27
+ it('initializes the native module', async () => {
28
+ await Amply.initialize({ appId: 'id', apiKeyPublic: 'public' });
29
+
30
+ expect(mockNativeModule.initialize).toHaveBeenCalledWith(
31
+ expect.objectContaining({ appId: 'id', apiKeyPublic: 'public' })
32
+ );
33
+ });
34
+
35
+ it('tracks events through the native layer', async () => {
36
+ await Amply.track({ name: 'Test Event', properties: { foo: 'bar' } });
37
+
38
+ expect(mockNativeModule.track).toHaveBeenCalledWith({
39
+ name: 'Test Event',
40
+ properties: { foo: 'bar' },
41
+ });
42
+ });
43
+
44
+ it('reads recent events', async () => {
45
+ const events = [{ name: 'e1' }];
46
+ mockNativeModule.getRecentEvents.mockResolvedValueOnce(events);
47
+
48
+ const result = await Amply.getRecentEvents(3);
49
+
50
+ expect(mockNativeModule.getRecentEvents).toHaveBeenCalledWith(3);
51
+ expect(result).toBe(events);
52
+ });
53
+
54
+ it('requests dataset snapshots', async () => {
55
+ const snapshot = { foo: 'bar' };
56
+ mockNativeModule.getDataSetSnapshot.mockResolvedValueOnce(snapshot);
57
+
58
+ const type = { kind: '@device' as const };
59
+ const result = await Amply.getDataSetSnapshot(type);
60
+
61
+ expect(mockNativeModule.getDataSetSnapshot).toHaveBeenCalledWith(type);
62
+ expect(result).toBe(snapshot);
63
+ });
64
+
65
+ it('subscribes to system events through the TurboModule emitter', async () => {
66
+ const remove = jest.fn();
67
+ mockNativeModule.onSystemEvent.mockReturnValueOnce({ remove });
68
+
69
+ const listener = jest.fn();
70
+ const unsubscribe = await Amply.addSystemEventListener(listener);
71
+
72
+ expect(mockNativeModule.onSystemEvent).toHaveBeenCalledWith(listener);
73
+ unsubscribe();
74
+ expect(remove).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+ it('registers deep links once and subscribes via the TurboModule emitter', async () => {
78
+ const remove = jest.fn();
79
+ mockNativeModule.onDeepLink.mockReturnValue({ remove });
80
+
81
+ const listener = jest.fn();
82
+ const unsubscribe = await Amply.addDeepLinkListener(listener);
83
+ expect(mockNativeModule.registerDeepLinkListener).toHaveBeenCalledTimes(1);
84
+ expect(mockNativeModule.onDeepLink).toHaveBeenCalledWith(listener);
85
+
86
+ await Amply.addDeepLinkListener(() => {});
87
+ expect(mockNativeModule.registerDeepLinkListener).toHaveBeenCalledTimes(1);
88
+
89
+ unsubscribe();
90
+ expect(remove).toHaveBeenCalledTimes(1);
91
+ });
92
+ });
@@ -0,0 +1,75 @@
1
+ import {useCallback, useEffect, useRef, useState} from 'react';
2
+ import type {EventRecord} from '../nativeSpecs/NativeAmplyModule';
3
+ import {addSystemEventListener} from '../systemEvents';
4
+
5
+ export type UseAmplySystemEventsOptions = {
6
+ maxEntries?: number;
7
+ dedupe?: boolean;
8
+ onEvent?: (event: EventRecord) => void;
9
+ };
10
+
11
+ export type UseAmplySystemEventsResult = {
12
+ events: EventRecord[];
13
+ reset: () => void;
14
+ };
15
+
16
+ export function useAmplySystemEvents(
17
+ options: UseAmplySystemEventsOptions = {},
18
+ ): UseAmplySystemEventsResult {
19
+ const {maxEntries = 50, dedupe = true, onEvent} = options;
20
+ const [events, setEvents] = useState<EventRecord[]>([]);
21
+ const seenKeysRef = useRef<Set<string>>(new Set());
22
+ const handlerRef = useRef<typeof onEvent>(onEvent);
23
+
24
+ useEffect(() => {
25
+ handlerRef.current = onEvent;
26
+ }, [onEvent]);
27
+
28
+ useEffect(() => {
29
+ let unsubscribe: (() => void) | undefined;
30
+ let isMounted = true;
31
+
32
+ addSystemEventListener(event => {
33
+ if (!isMounted) {
34
+ return;
35
+ }
36
+ if (dedupe) {
37
+ const key = `${event.name}-${event.timestamp}`;
38
+ if (seenKeysRef.current.has(key)) {
39
+ return;
40
+ }
41
+ seenKeysRef.current.add(key);
42
+ }
43
+ handlerRef.current?.(event);
44
+ setEvents(prev => {
45
+ const next = [...prev, event];
46
+ if (next.length > maxEntries) {
47
+ next.splice(0, next.length - maxEntries);
48
+ }
49
+ return next;
50
+ });
51
+ })
52
+ .then(unsub => {
53
+ if (!isMounted) {
54
+ unsub();
55
+ } else {
56
+ unsubscribe = unsub;
57
+ }
58
+ })
59
+ .catch(error => {
60
+ console.warn('[Amply] system event listener error', error);
61
+ });
62
+
63
+ return () => {
64
+ isMounted = false;
65
+ unsubscribe?.();
66
+ };
67
+ }, [dedupe, maxEntries]);
68
+
69
+ const reset = useCallback(() => {
70
+ seenKeysRef.current.clear();
71
+ setEvents([]);
72
+ }, []);
73
+
74
+ return {events, reset};
75
+ }