@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.
- package/AmplyReactNative.podspec +1 -1
- package/README.md +41 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +22 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +11 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +29 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/CampaignTouchPayload.kt +12 -0
- package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +8 -0
- package/android/src/newarch/jni/AmplyReactNative-generated.cpp +12 -0
- package/dist/cjs/campaignTouch.js +44 -0
- package/dist/cjs/campaignTouch.js.map +1 -0
- package/dist/cjs/hooks/useAmplySystemEvents.js +60 -0
- package/dist/cjs/hooks/useAmplySystemEvents.js.map +1 -0
- package/dist/cjs/index.js +723 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/nativeModule.js +55 -0
- package/dist/cjs/nativeModule.js.map +1 -0
- package/dist/cjs/nativeSpecs/NativeAmplyModule.js +5 -0
- package/dist/cjs/nativeSpecs/NativeAmplyModule.js.map +1 -0
- package/dist/cjs/systemEventUtils.js +47 -0
- package/dist/cjs/systemEventUtils.js.map +1 -0
- package/dist/cjs/systemEvents.js +27 -0
- package/dist/cjs/systemEvents.js.map +1 -0
- package/dist/src/campaignTouch.d.ts +36 -0
- package/dist/src/campaignTouch.d.ts.map +1 -0
- package/dist/src/campaignTouch.js +39 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +120 -0
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +28 -0
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -1
- package/dist/src/systemEventUtils.d.ts.map +1 -1
- package/dist/src/systemEventUtils.js +3 -1
- package/docs/ARCHITECTURE.md +28 -19
- package/docs/DEVELOPMENT.md +2 -1
- package/ios/Sources/AmplyReactNative/AmplyModule.mm +56 -0
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +14 -0
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +4 -0
- package/package.json +12 -6
- package/scripts/check-package.js +159 -0
- package/src/campaignTouch.ts +74 -0
- package/src/index.ts +124 -0
- package/src/nativeSpecs/NativeAmplyModule.ts +29 -0
- package/src/systemEventUtils.ts +3 -1
- package/dist/index.js +0 -272
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -234
- package/dist/index.mjs.map +0 -1
- package/dist/plugin/index.d.ts +0 -6
- package/dist/plugin/index.d.ts.map +0 -1
- package/dist/plugin/index.js +0 -186
- package/dist/plugin/index.js.map +0 -1
- package/dist/plugin/index.mjs +0 -169
- package/dist/plugin/index.mjs.map +0 -1
- package/dist/plugin/withAmply.d.ts +0 -12
- package/dist/plugin/withAmply.d.ts.map +0 -1
- package/dist/src/__tests__/appLanguage.test.d.ts +0 -43
- package/dist/src/__tests__/appLanguage.test.d.ts.map +0 -1
- package/dist/src/__tests__/appLanguage.test.js +0 -211
- package/dist/src/__tests__/campaignResolved.test.d.ts +0 -2
- package/dist/src/__tests__/campaignResolved.test.d.ts.map +0 -1
- package/dist/src/__tests__/campaignResolved.test.js +0 -208
- package/dist/src/__tests__/gateRegistrationLifecycle.test.d.ts +0 -2
- package/dist/src/__tests__/gateRegistrationLifecycle.test.d.ts.map +0 -1
- package/dist/src/__tests__/gateRegistrationLifecycle.test.js +0 -215
- package/dist/src/__tests__/index.test.d.ts +0 -2
- package/dist/src/__tests__/index.test.d.ts.map +0 -1
- package/dist/src/__tests__/index.test.js +0 -204
- package/dist/src/__tests__/systemEventUtils.test.d.ts +0 -2
- package/dist/src/__tests__/systemEventUtils.test.d.ts.map +0 -1
- package/dist/src/__tests__/systemEventUtils.test.js +0 -56
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/plugin/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/appLanguage.test.ts +0 -296
- package/src/__tests__/campaignResolved.test.ts +0 -294
- package/src/__tests__/gateRegistrationLifecycle.test.ts +0 -281
- package/src/__tests__/index.test.ts +0 -258
- package/src/__tests__/systemEventUtils.test.ts +0 -75
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
-
|
|
606
|
-
-
|
|
607
|
-
-
|
|
608
|
-
-
|
|
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
|
|
612
|
-
- `dist/index.
|
|
613
|
-
- `
|
|
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 (
|
|
720
|
+
expo-module build → tsc → dist/src/** (ESM + .d.ts), dist/plugin/**
|
|
709
721
|
↓
|
|
710
|
-
|
|
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
|
|
716
|
-
└─→ writes dist/src/**/*.d.ts (type declarations)
|
|
724
|
+
tsc -p tsconfig.cjs.json → dist/cjs/** (CommonJS, main)
|
|
717
725
|
↓
|
|
718
|
-
|
|
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
|
|
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/
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
+
}
|