@amplytools/react-native-amply-sdk 0.14.0 → 0.15.1

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 (78) hide show
  1. package/AmplyReactNative.podspec +1 -1
  2. package/README.md +41 -0
  3. package/android/build.gradle +1 -1
  4. package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +22 -0
  5. package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +11 -0
  6. package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +29 -0
  7. package/android/src/main/java/tools/amply/sdk/reactnative/model/CampaignTouchPayload.kt +12 -0
  8. package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +8 -0
  9. package/android/src/newarch/jni/AmplyReactNative-generated.cpp +12 -0
  10. package/dist/cjs/campaignTouch.js +44 -0
  11. package/dist/cjs/campaignTouch.js.map +1 -0
  12. package/dist/cjs/hooks/useAmplySystemEvents.js +60 -0
  13. package/dist/cjs/hooks/useAmplySystemEvents.js.map +1 -0
  14. package/dist/cjs/index.js +723 -0
  15. package/dist/cjs/index.js.map +1 -0
  16. package/dist/cjs/nativeModule.js +55 -0
  17. package/dist/cjs/nativeModule.js.map +1 -0
  18. package/dist/cjs/nativeSpecs/NativeAmplyModule.js +5 -0
  19. package/dist/cjs/nativeSpecs/NativeAmplyModule.js.map +1 -0
  20. package/dist/cjs/systemEventUtils.js +47 -0
  21. package/dist/cjs/systemEventUtils.js.map +1 -0
  22. package/dist/cjs/systemEvents.js +27 -0
  23. package/dist/cjs/systemEvents.js.map +1 -0
  24. package/dist/src/campaignTouch.d.ts +36 -0
  25. package/dist/src/campaignTouch.d.ts.map +1 -0
  26. package/dist/src/campaignTouch.js +39 -0
  27. package/dist/src/index.d.ts +35 -0
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +120 -0
  30. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +28 -0
  31. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -1
  32. package/dist/src/systemEventUtils.d.ts.map +1 -1
  33. package/dist/src/systemEventUtils.js +3 -1
  34. package/docs/ARCHITECTURE.md +28 -19
  35. package/docs/DEVELOPMENT.md +2 -1
  36. package/ios/Sources/AmplyReactNative/AmplyModule.mm +56 -0
  37. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +14 -0
  38. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +4 -0
  39. package/package.json +12 -6
  40. package/scripts/check-package.js +159 -0
  41. package/src/campaignTouch.ts +74 -0
  42. package/src/index.ts +124 -0
  43. package/src/nativeSpecs/NativeAmplyModule.ts +29 -0
  44. package/src/systemEventUtils.ts +3 -1
  45. package/dist/index.js +0 -272
  46. package/dist/index.js.map +0 -1
  47. package/dist/index.mjs +0 -234
  48. package/dist/index.mjs.map +0 -1
  49. package/dist/plugin/index.d.ts +0 -6
  50. package/dist/plugin/index.d.ts.map +0 -1
  51. package/dist/plugin/index.js +0 -186
  52. package/dist/plugin/index.js.map +0 -1
  53. package/dist/plugin/index.mjs +0 -169
  54. package/dist/plugin/index.mjs.map +0 -1
  55. package/dist/plugin/withAmply.d.ts +0 -12
  56. package/dist/plugin/withAmply.d.ts.map +0 -1
  57. package/dist/src/__tests__/appLanguage.test.d.ts +0 -43
  58. package/dist/src/__tests__/appLanguage.test.d.ts.map +0 -1
  59. package/dist/src/__tests__/appLanguage.test.js +0 -211
  60. package/dist/src/__tests__/campaignResolved.test.d.ts +0 -2
  61. package/dist/src/__tests__/campaignResolved.test.d.ts.map +0 -1
  62. package/dist/src/__tests__/campaignResolved.test.js +0 -208
  63. package/dist/src/__tests__/gateRegistrationLifecycle.test.d.ts +0 -2
  64. package/dist/src/__tests__/gateRegistrationLifecycle.test.d.ts.map +0 -1
  65. package/dist/src/__tests__/gateRegistrationLifecycle.test.js +0 -215
  66. package/dist/src/__tests__/index.test.d.ts +0 -2
  67. package/dist/src/__tests__/index.test.d.ts.map +0 -1
  68. package/dist/src/__tests__/index.test.js +0 -204
  69. package/dist/src/__tests__/systemEventUtils.test.d.ts +0 -2
  70. package/dist/src/__tests__/systemEventUtils.test.d.ts.map +0 -1
  71. package/dist/src/__tests__/systemEventUtils.test.js +0 -56
  72. package/dist/tsconfig.tsbuildinfo +0 -1
  73. package/plugin/tsconfig.tsbuildinfo +0 -1
  74. package/src/__tests__/appLanguage.test.ts +0 -296
  75. package/src/__tests__/campaignResolved.test.ts +0 -294
  76. package/src/__tests__/gateRegistrationLifecycle.test.ts +0 -281
  77. package/src/__tests__/index.test.ts +0 -258
  78. package/src/__tests__/systemEventUtils.test.ts +0 -75
@@ -150,6 +150,7 @@ export interface Spec extends TurboModule {
150
150
  isInitialized(): boolean;
151
151
  track(payload: TrackEventPayload): Promise<void>;
152
152
  getRecentEvents(limit: number): Promise<EventRecord[]>;
153
+ getLastCampaignTouch(withinMs: number): Promise<CampaignTouchPayload | null>;
153
154
  getDataSetSnapshot(type: DataSetType): Promise<DataSetSnapshot>;
154
155
  setUserId(userId: string | null): void;
155
156
  setAppLanguage(tag: string | null): void;
@@ -597,20 +598,29 @@ yarn codegen
597
598
  #### **2. JavaScript Bundling (TypeScript → JS)**
598
599
 
599
600
  ```bash
600
- yarn build
601
- # Internally runs: expo-module build (which uses tsup)
601
+ yarn build:package
602
+ # rm -rf dist plugin/build, then expo-module build (tsc), expo-module build plugin,
603
+ # tsc -p tsconfig.cjs.json, then scripts/check-package.js
602
604
  ```
603
605
 
604
606
  **What it does:**
605
- - Compiles TypeScript to JavaScript using `tsup`
606
- - Generates ES Modules (`.mjs`) and CommonJS (`.js`)
607
- - Type checks via `tsc` → generates `.d.ts` files
608
- - Outputs to `dist/` directory
607
+ - `expo-module build` runs `tsc` with `tsconfig.json`: ES modules plus `.d.ts` into `dist/src/` (and `dist/plugin/`)
608
+ - `expo-module build plugin` compiles the Expo plugin into `plugin/build/` (what `app.plugin.js` requires)
609
+ - `tsc -p tsconfig.cjs.json` writes a CommonJS copy of `src/` into `dist/cjs/`
610
+ - `scripts/check-package.js` checks the tarball `npm pack` would produce (below)
609
611
 
610
612
  **Outputs:**
611
- - `dist/index.js` – CommonJS entry point (consumers importing from Node)
612
- - `dist/index.mjs` – ES Module entry point (bundlers like Metro)
613
- - `dist/src/**/*.d.ts` – TypeScript declarations
613
+ - `dist/cjs/index.js` – `main`, CommonJS (Jest and anything else that `require`s the package)
614
+ - `dist/src/index.js` – `module`, ES modules (bundlers)
615
+ - `src/index.ts` – `react-native`; Metro compiles the source itself
616
+ - `dist/src/**/*.d.ts` – TypeScript declarations (`types`)
617
+
618
+ **The package check.** Until 0.15.0 `main` pointed at `dist/index.js`, a bundle from an early tsup build
619
+ that no later build rewrote or removed, so every release shipped the API of late 2025. Metro never
620
+ noticed (it reads `react-native`). `check-package.js` fails when an entry in package.json is not in the
621
+ tarball, when `main` does not export exactly what `src/index.ts` exports, or when a compiled file in the
622
+ tarball has no source file. `yarn test` runs it, and release.sh runs it before the release commit and
623
+ again right before `npm publish`.
614
624
 
615
625
  **When to run:**
616
626
  - After changing any TypeScript/JavaScript code
@@ -703,25 +713,23 @@ When you link the SDK to example apps, these are called automatically:
703
713
  ### Script Dependency Chain
704
714
 
705
715
  ```
706
- developer runs yarn build
716
+ developer runs yarn build:package
717
+ ↓
718
+ rm -rf dist plugin/build (no leftovers from an older build can ship)
707
719
  ↓
708
- expo-module build (wrapper script)
720
+ expo-module build → tsc → dist/src/** (ESM + .d.ts), dist/plugin/**
709
721
  ↓
710
- tsup (TypeScript bundler)
711
- ├─→ tsc (compile TS)
712
- ├─→ writes dist/index.js (CommonJS)
713
- └─→ writes dist/index.mjs (ES Module)
722
+ expo-module build plugin → plugin/build/**
714
723
  ↓
715
- tsc --project tsconfig.build.json
716
- └─→ writes dist/src/**/*.d.ts (type declarations)
724
+ tsc -p tsconfig.cjs.json → dist/cjs/** (CommonJS, main)
717
725
  ↓
718
- dist/ is ready for consumption
726
+ scripts/check-package.js (entries ship, main == src/index.ts exports, no orphans)
719
727
  ```
720
728
 
721
729
  ### Why So Many Compile Steps?
722
730
 
723
731
  1. **Codegen** – Generates native stubs (Java, C++, ObjC++)
724
- 2. **JS bundling** – Converts TS to JS for Node and Metro
732
+ 2. **JS compile** – Converts TS to JS for Node/Jest (CommonJS) and bundlers (ESM)
725
733
  3. **Type generation** – Creates `.d.ts` for consumers
726
734
  4. **Plugin build** – Separate build for Expo plugin code
727
735
 
@@ -961,6 +969,7 @@ src/
961
969
  ├─ index.ts # Public API (what users import)
962
970
  ├─ nativeModule.ts # Module loader
963
971
  ├─ systemEvents.ts # Event listener helper
972
+ ├─ campaignTouch.ts # CampaignTouch type + getLastCampaignTouch payload narrowing
964
973
  ├─ hooks/
965
974
  │ └─ useAmplySystemEvents.ts # React hook
966
975
  └─ nativeSpecs/
@@ -10,7 +10,8 @@ src/ # TypeScript source (JS side)
10
10
  │ └── NativeAmplyModule.ts # TurboModule spec (codegen source)
11
11
  ├── index.ts # Public API exports
12
12
  ├── nativeModule.ts # Native module loader with fallbacks
13
- └── systemEvents.ts # Event emitter helpers
13
+ ├── systemEvents.ts # Event emitter helpers
14
+ └── campaignTouch.ts # CampaignTouch type + payload narrowing
14
15
 
15
16
  android/ # Android native code
16
17
  ├── src/main/
@@ -659,6 +659,52 @@ static NSDictionary *AmplyGateDecisionToMap(ASDKGateDecision *decision) {
659
659
  }];
660
660
  }
661
661
 
662
+ // The JS spelling of a touch kind. A kind this bridge does not know is passed through lowercased
663
+ // rather than dropped here: the TS layer is the one place that decides what an unknown kind means.
664
+ static NSString *AmplyCampaignTouchKindToString(ASDKCampaignTouchKind *kind) {
665
+ if ([kind isEqual:ASDKCampaignTouchKind.shown]) return @"shown";
666
+ if ([kind isEqual:ASDKCampaignTouchKind.engaged]) return @"engaged";
667
+ return kind.name.lowercaseString ?: @"";
668
+ }
669
+
670
+ - (void)getLastCampaignTouch:(double)withinMs
671
+ resolve:(RCTPromiseResolveBlock)resolve
672
+ reject:(RCTPromiseRejectBlock)reject
673
+ {
674
+ // Not a rejection, unlike getRecentEvents: the SDK answers "not initialised" with null for this
675
+ // query, and the bridge keeps that contract instead of inventing an error for it.
676
+ if (!self.amplyInstance) {
677
+ if (resolve) {
678
+ resolve([NSNull null]);
679
+ }
680
+ return;
681
+ }
682
+
683
+ [self.amplyInstance getLastCampaignTouchWithinMs:(int64_t)withinMs
684
+ completionHandler:^(ASDKCampaignTouch *touch, NSError *error) {
685
+ if (error) {
686
+ RCTLogError(@"[AmplyReactNative] Failed to get last campaign touch: %@", error.localizedDescription);
687
+ if (reject) {
688
+ reject(@"AMP_CAMPAIGN_TOUCH_FAILED", error.localizedDescription, error);
689
+ }
690
+ return;
691
+ }
692
+ if (!resolve) {
693
+ return;
694
+ }
695
+ if (!touch) {
696
+ resolve([NSNull null]);
697
+ return;
698
+ }
699
+ resolve(@{
700
+ @"campaignId": touch.campaignId ?: @"",
701
+ @"campaignName": touch.campaignName ?: @"",
702
+ @"kind": AmplyCampaignTouchKindToString(touch.kind),
703
+ @"atMs": @(touch.atMs),
704
+ });
705
+ }];
706
+ }
707
+
662
708
  // Parses an array of `{name, value}` dicts into native EventParam objects.
663
709
  //
664
710
  // Bad-shape entries (non-dict, missing/non-string name) are silently dropped — this
@@ -1048,6 +1094,16 @@ static ASDKDataSetType *AmplyDataSetTypeFromDictionary(NSDictionary *type, NSStr
1048
1094
  [[ASDKLogger shared] setLevelLevel:level];
1049
1095
  }
1050
1096
 
1097
+ - (NSNumber *)handleUrl:(NSString *)url
1098
+ {
1099
+ // Not buffered: a link is only meaningful to a live SDK, and the answer is needed right now.
1100
+ ASDKAmply *instance = self.amplyInstance;
1101
+ if (instance == nil || url == nil) {
1102
+ return @NO;
1103
+ }
1104
+ return @([instance handleUrlUrl:url]);
1105
+ }
1106
+
1051
1107
  - (NSString *)getLogLevel
1052
1108
  {
1053
1109
  return AmplyLogLevelToString(self.currentLogLevel);
@@ -80,6 +80,10 @@ namespace facebook::react {
80
80
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "getRecentEvents", @selector(getRecentEvents:resolve:reject:), args, count);
81
81
  }
82
82
 
83
+ static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_getLastCampaignTouch(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
84
+ return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "getLastCampaignTouch", @selector(getLastCampaignTouch:resolve:reject:), args, count);
85
+ }
86
+
83
87
  static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_getDataSetSnapshot(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
84
88
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "getDataSetSnapshot", @selector(getDataSetSnapshot:resolve:reject:), args, count);
85
89
  }
@@ -96,6 +100,10 @@ namespace facebook::react {
96
100
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "setAppLanguage", @selector(setAppLanguage:), args, count);
97
101
  }
98
102
 
103
+ static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_handleUrl(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
104
+ return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, BooleanKind, "handleUrl", @selector(handleUrl:), args, count);
105
+ }
106
+
99
107
  static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_setLogLevel(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
100
108
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "setLogLevel", @selector(setLogLevel:), args, count);
101
109
  }
@@ -155,6 +163,9 @@ namespace facebook::react {
155
163
  methodMap_["getRecentEvents"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_getRecentEvents};
156
164
 
157
165
 
166
+ methodMap_["getLastCampaignTouch"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_getLastCampaignTouch};
167
+
168
+
158
169
  methodMap_["getDataSetSnapshot"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_getDataSetSnapshot};
159
170
 
160
171
 
@@ -167,6 +178,9 @@ namespace facebook::react {
167
178
  methodMap_["setAppLanguage"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_setAppLanguage};
168
179
 
169
180
 
181
+ methodMap_["handleUrl"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_handleUrl};
182
+
183
+
170
184
  methodMap_["setLogLevel"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_setLogLevel};
171
185
 
172
186
 
@@ -94,12 +94,16 @@ namespace JS {
94
94
  - (void)getRecentEvents:(double)limit
95
95
  resolve:(RCTPromiseResolveBlock)resolve
96
96
  reject:(RCTPromiseRejectBlock)reject;
97
+ - (void)getLastCampaignTouch:(double)withinMs
98
+ resolve:(RCTPromiseResolveBlock)resolve
99
+ reject:(RCTPromiseRejectBlock)reject;
97
100
  - (void)getDataSetSnapshot:(NSDictionary *)type
98
101
  resolve:(RCTPromiseResolveBlock)resolve
99
102
  reject:(RCTPromiseRejectBlock)reject;
100
103
  - (void)registerDeepLinkListener;
101
104
  - (void)setUserId:(NSString * _Nullable)userId;
102
105
  - (void)setAppLanguage:(NSString * _Nullable)tag;
106
+ - (NSNumber *)handleUrl:(NSString *)url;
103
107
  - (void)setLogLevel:(NSString *)level;
104
108
  - (NSString *)getLogLevel;
105
109
  - (void)setCustomProperties:(NSDictionary *)properties;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amplytools/react-native-amply-sdk",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "React Native SDK for Amply: Mobile SDK to orchestrate in-app experiences and campaigns remotely, without app releases",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,8 +11,8 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/amply-tools/amply-react-native/issues"
13
13
  },
14
- "main": "dist/index.js",
15
- "module": "dist/index.mjs",
14
+ "main": "dist/cjs/index.js",
15
+ "module": "dist/src/index.js",
16
16
  "types": "dist/src/index.d.ts",
17
17
  "react-native": "src/index.ts",
18
18
  "source": "src/index.ts",
@@ -27,19 +27,25 @@
27
27
  "react-native.config.js",
28
28
  "app.plugin.js",
29
29
  "README.md",
30
- "docs/"
30
+ "docs/",
31
+ "!dist/**/__tests__",
32
+ "!src/**/__tests__",
33
+ "!**/*.tsbuildinfo"
31
34
  ],
32
35
  "scripts": {
33
36
  "build": "expo-module build",
34
37
  "build:plugin": "expo-module build plugin",
38
+ "build:cjs": "tsc -p tsconfig.cjs.json",
39
+ "build:package": "rm -rf dist plugin/build && EXPO_NONINTERACTIVE=1 expo-module build && EXPO_NONINTERACTIVE=1 expo-module build plugin && tsc -p tsconfig.cjs.json && node scripts/check-package.js",
35
40
  "clean": "expo-module clean",
36
- "test": "node scripts/codegen.js --check && expo-module test && yarn check:seam-tokens && yarn check:sample-parity",
41
+ "test": "node scripts/codegen.js --check && expo-module test && yarn check:seam-tokens && yarn check:sample-parity && yarn build:package",
37
42
  "prepare": "expo-module prepare",
38
43
  "prepublishOnly": "expo-module prepublishOnly",
39
44
  "codegen": "node scripts/codegen.js",
40
45
  "codegen:check": "node scripts/codegen.js --check",
41
46
  "check:seam-tokens": "bash scripts/check-seam-tokens.sh",
42
- "check:sample-parity": "bash scripts/check-sample-parity.sh"
47
+ "check:sample-parity": "bash scripts/check-sample-parity.sh",
48
+ "check:package": "node scripts/check-package.js"
43
49
  },
44
50
  "peerDependencies": {
45
51
  "expo": ">=54.0.0",
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Checks the tarball `npm publish` would ship, after a build: every entry point in package.json is
4
+ * in it, and every compiled file in it comes from a source file in this repo.
5
+ *
6
+ * Why: until 0.15.0 `main` pointed at `dist/index.js`, a bundle an early tsup build had
7
+ * left in the working tree. `expo-module build` writes to `dist/src/`, so nothing ever rebuilt or
8
+ * removed that file, and every release shipped it — `main` served the API of late 2025 (no
9
+ * `handleUrl`, no `getLastCampaignTouch`). Metro reads `react-native` and never noticed; Jest and
10
+ * anything else that resolves `main` did. A leftover like that has no source file behind it, which
11
+ * is what the second check looks for.
12
+ *
13
+ * `yarn build:package` runs it after a clean build; `yarn test` and release.sh (before anything is
14
+ * pushed, and again right before `npm publish`) run that.
15
+ * Exits non-zero and names each problem.
16
+ */
17
+ const {execFileSync} = require('child_process');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const root = path.resolve(__dirname, '..');
22
+ process.chdir(root);
23
+
24
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
25
+
26
+ // The tsc builds and where their sources live: `dist/cjs/` mirrors `src/` (tsconfig.cjs.json),
27
+ // the rest of `dist/` mirrors the repo root (rootDir "."), `plugin/build/` mirrors `plugin/src/`.
28
+ // First match wins, so the narrower `dist/cjs/` comes before `dist/`.
29
+ const OUTPUTS = [
30
+ {outDir: 'dist/cjs/', sourceDir: 'src/'},
31
+ {outDir: 'dist/', sourceDir: ''},
32
+ {outDir: 'plugin/build/', sourceDir: 'plugin/src/'},
33
+ ];
34
+ const COMPILED = /(\.d\.ts\.map|\.d\.ts|\.js\.map|\.mjs\.map|\.js|\.mjs)$/;
35
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx'];
36
+
37
+ const packed = JSON.parse(packList())[0].files.map((file) => file.path);
38
+ const shipped = new Set(packed);
39
+
40
+ const problems = [];
41
+
42
+ const entries = {
43
+ main: pkg.main,
44
+ types: pkg.types,
45
+ 'react-native': pkg['react-native'],
46
+ source: pkg.source,
47
+ module: pkg.module,
48
+ 'expo.plugin': pkg.expo?.plugin?.replace(/^\.\//, ''),
49
+ };
50
+ for (const [field, entry] of Object.entries(entries)) {
51
+ if (entry !== undefined && !shipped.has(entry)) {
52
+ problems.push(`package.json "${field}" points at ${entry}, which is not in the tarball`);
53
+ }
54
+ }
55
+
56
+ // app.plugin.js is a one-line require of the compiled plugin; that target has to ship too.
57
+ const pluginTarget = /require\('\.\/([^']+)'\)/.exec(fs.readFileSync('app.plugin.js', 'utf8'))?.[1];
58
+ if (pluginTarget && !shipped.has(`${pluginTarget}/index.js`) && !shipped.has(`${pluginTarget}.js`)) {
59
+ problems.push(`app.plugin.js requires ./${pluginTarget}, which is not in the tarball`);
60
+ }
61
+
62
+ // `main` must serve the API of this tree's src/index.ts, name for name. It is loaded the way Jest
63
+ // loads it (plain require), with the peers stubbed: nothing in the SDK touches native at import.
64
+ const sourceExports = exportedNames('src/index.ts');
65
+ const mainExports = requiredNames(pkg.main);
66
+ const missing = sourceExports.filter((name) => !mainExports.includes(name));
67
+ const extra = mainExports.filter((name) => !sourceExports.includes(name));
68
+ if (missing.length > 0 || extra.length > 0) {
69
+ problems.push(
70
+ `"main" (${pkg.main}) does not serve src/index.ts — missing: [${missing.join(', ')}], extra: [${extra.join(', ')}]`,
71
+ );
72
+ }
73
+
74
+ for (const file of packed) {
75
+ const output = OUTPUTS.find(({outDir}) => file.startsWith(outDir));
76
+ if (!output || !COMPILED.test(file)) {
77
+ continue;
78
+ }
79
+ const stem = output.sourceDir + file.slice(output.outDir.length).replace(COMPILED, '');
80
+ if (!SOURCE_EXTENSIONS.some((extension) => fs.existsSync(stem + extension))) {
81
+ problems.push(`${file} has no source (${stem}.ts) — a leftover from an older build`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * `npm pack --dry-run --json`. The bare example's assembleDebug writes android/.cxx into the
87
+ * package root, and with it present npm 11 dies with "Exit handler never called!" (measured
88
+ * 2026-10-17: rc 1 with android/.cxx, rc 0 without it). That failure is npm's, not the package's —
89
+ * say so and name the way out instead of dying on a stack trace.
90
+ */
91
+ function packList() {
92
+ try {
93
+ return execFileSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], {
94
+ encoding: 'utf8',
95
+ stdio: ['ignore', 'pipe', 'pipe'],
96
+ });
97
+ } catch (error) {
98
+ const npmErrors = String(error.stderr ?? '')
99
+ .split('\n')
100
+ .filter((line) => line.startsWith('npm error'))
101
+ .slice(0, 3);
102
+ console.error(`FAIL: npm pack --dry-run failed:\n ${npmErrors.join('\n ')}`);
103
+ console.error('check-package: a native build in the package root is the usual cause — rm -rf android/build android/.cxx ios/build');
104
+ process.exit(1);
105
+ }
106
+ }
107
+
108
+ /** The value exports of a TypeScript module, as the compiler sees them (types are not runtime). */
109
+ function exportedNames(file) {
110
+ const ts = require('typescript');
111
+ const program = ts.createProgram([file], {allowJs: false, jsx: ts.JsxEmit.ReactNative, skipLibCheck: true});
112
+ const checker = program.getTypeChecker();
113
+ const moduleSymbol = checker.getSymbolAtLocation(program.getSourceFile(file));
114
+ return checker
115
+ .getExportsOfModule(moduleSymbol)
116
+ .filter((symbol) => {
117
+ const target = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
118
+ return (target.flags & ts.SymbolFlags.Value) !== 0;
119
+ })
120
+ .map((symbol) => symbol.getName())
121
+ .sort();
122
+ }
123
+
124
+ /** The export names of a CommonJS file when required with react / react-native stubbed out. */
125
+ function requiredNames(file) {
126
+ const Module = require('module');
127
+ const load = Module._load;
128
+ const stubs = {
129
+ react: {useEffect() {}, useRef: () => ({}), useState: () => [], useCallback: (callback) => callback},
130
+ 'react-native': {
131
+ NativeModules: {},
132
+ TurboModuleRegistry: {},
133
+ Platform: {OS: 'ios'},
134
+ NativeEventEmitter: class {},
135
+ Linking: {},
136
+ },
137
+ };
138
+ Module._load = function loadWithStubs(request, ...rest) {
139
+ return request in stubs ? stubs[request] : load.call(this, request, ...rest);
140
+ };
141
+ globalThis.__DEV__ = false;
142
+ try {
143
+ return Object.keys(require(path.resolve(file))).sort();
144
+ } catch (error) {
145
+ problems.push(`"main" (${file}) cannot be required: ${error.message.split('\n')[0]}`);
146
+ return [];
147
+ } finally {
148
+ Module._load = load;
149
+ }
150
+ }
151
+
152
+ if (problems.length > 0) {
153
+ for (const problem of problems) {
154
+ console.error(`FAIL: ${problem}`);
155
+ }
156
+ console.error('check-package: rebuild from a clean tree (rm -rf dist plugin/build) or fix the entry.');
157
+ process.exit(1);
158
+ }
159
+ console.log(`check-package: OK — ${packed.length} files, every entry ships, every compiled file has a source.`);
@@ -0,0 +1,74 @@
1
+ import type {CampaignTouchPayload} from './nativeSpecs/NativeAmplyModule';
2
+
3
+ /**
4
+ * - `'shown'`: the campaign was shown (`CampaignShown`).
5
+ * - `'engaged'`: the campaign resolved `Completed` (`CampaignResolved` with `result: 'Completed'`).
6
+ */
7
+ export type CampaignTouchKind = 'shown' | 'engaged';
8
+
9
+ /**
10
+ * The most recent moment a campaign reached the user, as answered by `getLastCampaignTouch`.
11
+ *
12
+ * Meant for your own analytics: attach `campaignId` / `campaignName` to a purchase (or any
13
+ * conversion) that happens shortly after a campaign, even when it did not start from the
14
+ * campaign's own link.
15
+ */
16
+ export type CampaignTouch = {
17
+ /**
18
+ * The id the SDK reports in `CampaignShown`. Inside an experiment that is the arm's own id —
19
+ * the same value `{@campaign.id}` resolves to, so it is also the right value for an explicit
20
+ * `amply_campaign_id` mark.
21
+ */
22
+ campaignId: string;
23
+ /** The campaign's name when the touch was recorded. Empty if the event carried no name. */
24
+ campaignName: string;
25
+ /** One touch per campaign: the newer of a show and a completion wins. */
26
+ kind: CampaignTouchKind;
27
+ /** When it happened, epoch milliseconds in the SDK's server-corrected clock. */
28
+ atMs: number;
29
+ };
30
+
31
+ /** Thrown (as a rejection) by `getLastCampaignTouch` for a window that is not a usable number. */
32
+ export function assertValidWithinMs(withinMs: unknown): asserts withinMs is number {
33
+ if (typeof withinMs !== 'number' || !Number.isFinite(withinMs) || withinMs < 0) {
34
+ throw new RangeError(
35
+ `[Amply] getLastCampaignTouch: withinMs must be a finite number >= 0, got ${String(withinMs)}`,
36
+ );
37
+ }
38
+ }
39
+
40
+ function toKind(raw: unknown): CampaignTouchKind | null {
41
+ if (typeof raw !== 'string') {
42
+ return null;
43
+ }
44
+ const lowered = raw.toLowerCase();
45
+ return lowered === 'shown' || lowered === 'engaged' ? lowered : null;
46
+ }
47
+
48
+ /**
49
+ * Narrows what the native bridge resolved to the public type. Anything it cannot read as a
50
+ * touch becomes null with a warning: the caller asked "was there a campaign", and a payload this
51
+ * bridge does not understand is not one it can vouch for.
52
+ */
53
+ export function toCampaignTouch(payload: CampaignTouchPayload | null | undefined): CampaignTouch | null {
54
+ if (payload == null) {
55
+ return null;
56
+ }
57
+ const kind = toKind(payload.kind);
58
+ const isReadable =
59
+ kind !== null &&
60
+ typeof payload.campaignId === 'string' &&
61
+ payload.campaignId.length > 0 &&
62
+ typeof payload.atMs === 'number' &&
63
+ Number.isFinite(payload.atMs);
64
+ if (!isReadable) {
65
+ console.warn('[Amply] getLastCampaignTouch: ignoring an unreadable native payload', payload);
66
+ return null;
67
+ }
68
+ return {
69
+ campaignId: payload.campaignId,
70
+ campaignName: typeof payload.campaignName === 'string' ? payload.campaignName : '',
71
+ kind,
72
+ atMs: payload.atMs,
73
+ };
74
+ }