@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
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import * as Amply from '../index';
|
|
2
|
-
import { __setNativeModule } from '../nativeModule';
|
|
3
|
-
async function flushMicrotasks() {
|
|
4
|
-
for (let i = 0; i < 5; i++) {
|
|
5
|
-
await Promise.resolve();
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* The registration lifecycle of a gate: who receives a presentation, what withdrawal
|
|
10
|
-
* actually withdraws, and what happens to a registration made before the SDK is up.
|
|
11
|
-
*
|
|
12
|
-
* All three were found on 2026-07-31 — the first two by reading the bridge after a field
|
|
13
|
-
* report on Happens, the third by the field report itself. They share one cause: the JS side
|
|
14
|
-
* keeps its own record of what is registered natively, and that record was written
|
|
15
|
-
* optimistically and never reconciled.
|
|
16
|
-
*
|
|
17
|
-
* Trello: gate eviction https://trello.com/c/Gq589zwz, withdrawal
|
|
18
|
-
* https://trello.com/c/saf8I9c6, pre-init registration https://trello.com/c/TZCixnYi
|
|
19
|
-
*/
|
|
20
|
-
describe('gate registration lifecycle', () => {
|
|
21
|
-
/** Every listener currently attached to the shared onCampaignPresent channel. */
|
|
22
|
-
let emitters;
|
|
23
|
-
const mockNativeModule = {
|
|
24
|
-
initialize: jest.fn(),
|
|
25
|
-
isInitialized: jest.fn(() => true),
|
|
26
|
-
track: jest.fn(),
|
|
27
|
-
trackGated: jest.fn(),
|
|
28
|
-
resolveCampaign: jest.fn(),
|
|
29
|
-
getRecentEvents: jest.fn(),
|
|
30
|
-
getDataSetSnapshot: jest.fn(),
|
|
31
|
-
registerDeepLinkListener: jest.fn(),
|
|
32
|
-
registerGate: jest.fn(),
|
|
33
|
-
unregisterGate: jest.fn(),
|
|
34
|
-
addListener: jest.fn(),
|
|
35
|
-
removeListeners: jest.fn(),
|
|
36
|
-
onSystemEvent: jest.fn(),
|
|
37
|
-
onDeepLink: jest.fn(),
|
|
38
|
-
onCampaignPresent: jest.fn(),
|
|
39
|
-
setCustomProperties: jest.fn(),
|
|
40
|
-
getCustomProperty: jest.fn(),
|
|
41
|
-
removeCustomProperty: jest.fn(),
|
|
42
|
-
clearCustomProperties: jest.fn(),
|
|
43
|
-
};
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
emitters = [];
|
|
46
|
-
mockNativeModule.isInitialized.mockReturnValue(true);
|
|
47
|
-
mockNativeModule.onCampaignPresent.mockImplementation((listener) => {
|
|
48
|
-
emitters.push(listener);
|
|
49
|
-
// `remove` must actually detach, as a real RN subscription does — otherwise this
|
|
50
|
-
// harness reports a listener as live after it was removed, and every eviction and
|
|
51
|
-
// withdrawal assertion below would be measuring the mock rather than the code.
|
|
52
|
-
const remove = jest.fn(() => {
|
|
53
|
-
const at = emitters.indexOf(listener);
|
|
54
|
-
if (at >= 0) {
|
|
55
|
-
emitters.splice(at, 1);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
return { remove };
|
|
59
|
-
});
|
|
60
|
-
mockNativeModule.onDeepLink.mockReturnValue({ remove: jest.fn() });
|
|
61
|
-
__setNativeModule(mockNativeModule);
|
|
62
|
-
});
|
|
63
|
-
afterEach(() => {
|
|
64
|
-
Amply.removeAllListeners();
|
|
65
|
-
jest.clearAllMocks();
|
|
66
|
-
__setNativeModule(null);
|
|
67
|
-
});
|
|
68
|
-
/** Delivers one presentation to every listener currently subscribed to the channel. */
|
|
69
|
-
function emitPresentation(mediationId = 'm1') {
|
|
70
|
-
const event = {
|
|
71
|
-
url: 'stillframe://ad?adUnit=main',
|
|
72
|
-
params: { adUnit: 'main' },
|
|
73
|
-
info: { campaignId: 'c1' },
|
|
74
|
-
mediationId,
|
|
75
|
-
};
|
|
76
|
-
emitters.forEach(emit => emit(event));
|
|
77
|
-
}
|
|
78
|
-
describe('a second registration on the same baseUrl (Gq589zwz)', () => {
|
|
79
|
-
it('leaves exactly one live presenter, not two', async () => {
|
|
80
|
-
const first = jest.fn((_p, _i, r) => r.completed());
|
|
81
|
-
const second = jest.fn((_p, _i, r) => r.completed());
|
|
82
|
-
await Amply.registerGate('stillframe://ad', first);
|
|
83
|
-
await Amply.registerGate('stillframe://ad', second);
|
|
84
|
-
emitPresentation();
|
|
85
|
-
await flushMicrotasks();
|
|
86
|
-
// Both presenters showing UI for one presentation is the defect: the outcome is then
|
|
87
|
-
// decided by whichever reported first, and the loser's UI is orphaned on screen.
|
|
88
|
-
expect(second).toHaveBeenCalledTimes(1);
|
|
89
|
-
expect(first).not.toHaveBeenCalled();
|
|
90
|
-
});
|
|
91
|
-
it('applies the options of the newest registration', async () => {
|
|
92
|
-
await Amply.registerGate('stillframe://ad', jest.fn(), {
|
|
93
|
-
onAbort: 'cancel',
|
|
94
|
-
timeoutMs: 1000,
|
|
95
|
-
});
|
|
96
|
-
await Amply.registerGate('stillframe://ad', jest.fn(), {
|
|
97
|
-
onAbort: 'proceed',
|
|
98
|
-
timeoutMs: 8000,
|
|
99
|
-
});
|
|
100
|
-
// Previously the second call was a no-op natively, so its onAbort and timeout were
|
|
101
|
-
// silently discarded and the gate kept running under the first call's policy.
|
|
102
|
-
expect(mockNativeModule.registerGate).toHaveBeenLastCalledWith('stillframe://ad', 'proceed', 8000);
|
|
103
|
-
});
|
|
104
|
-
it('keeps gates on different baseUrls independent', async () => {
|
|
105
|
-
const ad = jest.fn((_p, _i, r) => r.completed());
|
|
106
|
-
const paywall = jest.fn((_p, _i, r) => r.completed());
|
|
107
|
-
await Amply.registerGate('stillframe://ad', ad);
|
|
108
|
-
await Amply.registerGate('stillframe://paywall', paywall);
|
|
109
|
-
emitPresentation();
|
|
110
|
-
await flushMicrotasks();
|
|
111
|
-
// Only the URL-matching presenter runs; evicting by baseUrl must not evict a sibling.
|
|
112
|
-
expect(ad).toHaveBeenCalledTimes(1);
|
|
113
|
-
expect(paywall).not.toHaveBeenCalled();
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe('withdrawal (saf8I9c6)', () => {
|
|
117
|
-
it('withdraws the native gate, not just the JS subscription', async () => {
|
|
118
|
-
const unsubscribe = await Amply.registerGate('stillframe://ad', jest.fn());
|
|
119
|
-
unsubscribe();
|
|
120
|
-
// Without this the gate outlives its presenter and parks the next gated call on that
|
|
121
|
-
// URL for the whole fail-open timeout — silently, with no crash and no console trace.
|
|
122
|
-
expect(mockNativeModule.unregisterGate).toHaveBeenCalledWith('stillframe://ad');
|
|
123
|
-
});
|
|
124
|
-
it('lets the same baseUrl be registered again after withdrawal', async () => {
|
|
125
|
-
const unsubscribe = await Amply.registerGate('stillframe://ad', jest.fn());
|
|
126
|
-
unsubscribe();
|
|
127
|
-
const presenter = jest.fn((_p, _i, r) => r.completed());
|
|
128
|
-
await Amply.registerGate('stillframe://ad', presenter);
|
|
129
|
-
emitPresentation();
|
|
130
|
-
await flushMicrotasks();
|
|
131
|
-
expect(presenter).toHaveBeenCalledTimes(1);
|
|
132
|
-
});
|
|
133
|
-
/**
|
|
134
|
-
* Found by codex, not by me. Withdrawal was keyed by URL alone, so an unsubscribe handed
|
|
135
|
-
* out for a registration that has since been evicted still withdrew whatever holds that
|
|
136
|
-
* URL now — killing its replacement. A stale handle must be inert, not destructive.
|
|
137
|
-
*/
|
|
138
|
-
it('an evicted registration\'s unsubscribe does not withdraw its replacement', async () => {
|
|
139
|
-
const withdrawFirst = await Amply.registerGate('stillframe://ad', jest.fn());
|
|
140
|
-
const second = jest.fn((_p, _i, r) => r.completed());
|
|
141
|
-
await Amply.registerGate('stillframe://ad', second);
|
|
142
|
-
mockNativeModule.unregisterGate.mockClear();
|
|
143
|
-
withdrawFirst();
|
|
144
|
-
expect(mockNativeModule.unregisterGate).not.toHaveBeenCalled();
|
|
145
|
-
emitPresentation();
|
|
146
|
-
await flushMicrotasks();
|
|
147
|
-
expect(second).toHaveBeenCalledTimes(1);
|
|
148
|
-
});
|
|
149
|
-
it('is idempotent — a second call withdraws nothing further', async () => {
|
|
150
|
-
const unsubscribe = await Amply.registerGate('stillframe://ad', jest.fn());
|
|
151
|
-
unsubscribe();
|
|
152
|
-
unsubscribe();
|
|
153
|
-
expect(mockNativeModule.unregisterGate).toHaveBeenCalledTimes(1);
|
|
154
|
-
});
|
|
155
|
-
it('removeAllListeners withdraws every native gate too', async () => {
|
|
156
|
-
await Amply.registerGate('stillframe://ad', jest.fn());
|
|
157
|
-
await Amply.registerGate('stillframe://paywall', jest.fn());
|
|
158
|
-
Amply.removeAllListeners();
|
|
159
|
-
// It used to clear the JS bookkeeping only, leaving JS believing nothing was
|
|
160
|
-
// registered while both gates were still live natively.
|
|
161
|
-
expect(mockNativeModule.unregisterGate).toHaveBeenCalledWith('stillframe://ad');
|
|
162
|
-
expect(mockNativeModule.unregisterGate).toHaveBeenCalledWith('stillframe://paywall');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
describe('registration before initialize (TZCixnYi)', () => {
|
|
166
|
-
it('replays a gate registration once the SDK is up', async () => {
|
|
167
|
-
mockNativeModule.isInitialized.mockReturnValue(false);
|
|
168
|
-
const presenter = jest.fn((_p, _i, r) => r.completed());
|
|
169
|
-
await Amply.registerGate('stillframe://ad', presenter);
|
|
170
|
-
// Native refuses while uninitialised; JS must not record that as done.
|
|
171
|
-
expect(mockNativeModule.registerGate).not.toHaveBeenCalled();
|
|
172
|
-
mockNativeModule.isInitialized.mockReturnValue(true);
|
|
173
|
-
await Amply.initialize({ appId: 'a', apiKeyPublic: 'k', apiKeySecret: 's' });
|
|
174
|
-
expect(mockNativeModule.registerGate).toHaveBeenCalledWith('stillframe://ad', 'cancel', 0);
|
|
175
|
-
emitPresentation();
|
|
176
|
-
await flushMicrotasks();
|
|
177
|
-
expect(presenter).toHaveBeenCalledTimes(1);
|
|
178
|
-
});
|
|
179
|
-
it('replays a deeplink listener once the SDK is up', async () => {
|
|
180
|
-
mockNativeModule.isInitialized.mockReturnValue(false);
|
|
181
|
-
await Amply.addDeepLinkListener(jest.fn());
|
|
182
|
-
expect(mockNativeModule.registerDeepLinkListener).not.toHaveBeenCalled();
|
|
183
|
-
mockNativeModule.isInitialized.mockReturnValue(true);
|
|
184
|
-
await Amply.initialize({ appId: 'a', apiKeyPublic: 'k', apiKeySecret: 's' });
|
|
185
|
-
expect(mockNativeModule.registerDeepLinkListener).toHaveBeenCalledTimes(1);
|
|
186
|
-
});
|
|
187
|
-
/**
|
|
188
|
-
* Found by self-review, not by the original pass. Eviction ran at `registerGate()` time,
|
|
189
|
-
* but a queued registration is not in the registry yet, so there was nothing to evict —
|
|
190
|
-
* both attached at replay and the second overwrote the map entry without removing the
|
|
191
|
-
* first subscription. That is the duplicate-presenter defect again, on the pre-init path.
|
|
192
|
-
*/
|
|
193
|
-
it('two registrations on one baseUrl before init still leave one live presenter', async () => {
|
|
194
|
-
mockNativeModule.isInitialized.mockReturnValue(false);
|
|
195
|
-
const first = jest.fn((_p, _i, r) => r.completed());
|
|
196
|
-
const second = jest.fn((_p, _i, r) => r.completed());
|
|
197
|
-
await Amply.registerGate('stillframe://ad', first);
|
|
198
|
-
await Amply.registerGate('stillframe://ad', second);
|
|
199
|
-
mockNativeModule.isInitialized.mockReturnValue(true);
|
|
200
|
-
await Amply.initialize({ appId: 'a', apiKeyPublic: 'k', apiKeySecret: 's' });
|
|
201
|
-
emitPresentation();
|
|
202
|
-
await flushMicrotasks();
|
|
203
|
-
expect(second).toHaveBeenCalledTimes(1);
|
|
204
|
-
expect(first).not.toHaveBeenCalled();
|
|
205
|
-
});
|
|
206
|
-
it('withdrawing a queued gate cancels it instead of replaying it', async () => {
|
|
207
|
-
mockNativeModule.isInitialized.mockReturnValue(false);
|
|
208
|
-
const unsubscribe = await Amply.registerGate('stillframe://ad', jest.fn());
|
|
209
|
-
unsubscribe();
|
|
210
|
-
mockNativeModule.isInitialized.mockReturnValue(true);
|
|
211
|
-
await Amply.initialize({ appId: 'a', apiKeyPublic: 'k', apiKeySecret: 's' });
|
|
212
|
-
expect(mockNativeModule.registerGate).not.toHaveBeenCalled();
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import * as Amply from '../index';
|
|
2
|
-
import { __setNativeModule } from '../nativeModule';
|
|
3
|
-
const mockNativeModule = {
|
|
4
|
-
initialize: jest.fn(),
|
|
5
|
-
isInitialized: jest.fn(() => true),
|
|
6
|
-
track: jest.fn(),
|
|
7
|
-
getRecentEvents: jest.fn(),
|
|
8
|
-
getDataSetSnapshot: jest.fn(),
|
|
9
|
-
registerDeepLinkListener: jest.fn(),
|
|
10
|
-
addListener: jest.fn(),
|
|
11
|
-
removeListeners: jest.fn(),
|
|
12
|
-
onSystemEvent: jest.fn(),
|
|
13
|
-
onDeepLink: jest.fn(),
|
|
14
|
-
setCustomProperties: jest.fn(),
|
|
15
|
-
getCustomProperty: jest.fn(),
|
|
16
|
-
removeCustomProperty: jest.fn(),
|
|
17
|
-
clearCustomProperties: jest.fn(),
|
|
18
|
-
};
|
|
19
|
-
describe('Amply JS API', () => {
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
jest.clearAllMocks();
|
|
22
|
-
__setNativeModule(null);
|
|
23
|
-
});
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
__setNativeModule(mockNativeModule);
|
|
26
|
-
});
|
|
27
|
-
it('initializes the native module', async () => {
|
|
28
|
-
await Amply.initialize({ appId: 'id', apiKeyPublic: 'public', apiKeySecret: 'secret' });
|
|
29
|
-
expect(mockNativeModule.initialize).toHaveBeenCalledWith(expect.objectContaining({ appId: 'id', apiKeyPublic: 'public' }));
|
|
30
|
-
});
|
|
31
|
-
// A missing key used to reach the native SDK, whose config builder threw an exception that
|
|
32
|
-
// crossed into Objective-C uncaught and aborted the host app — no JS try/catch could stop it.
|
|
33
|
-
// Bad keys must cost the app its Amply, never its process.
|
|
34
|
-
describe.each([
|
|
35
|
-
['apiKeySecret', { appId: 'id', apiKeyPublic: 'public' }],
|
|
36
|
-
['apiKeySecret', { appId: 'id', apiKeyPublic: 'public', apiKeySecret: '' }],
|
|
37
|
-
['apiKeySecret', { appId: 'id', apiKeyPublic: 'public', apiKeySecret: ' ' }],
|
|
38
|
-
['apiKeySecret', { appId: 'id', apiKeyPublic: 'public', apiKeySecret: null }],
|
|
39
|
-
['apiKeyPublic', { appId: 'id', apiKeyPublic: '', apiKeySecret: 'secret' }],
|
|
40
|
-
['appId', { appId: '', apiKeyPublic: 'public', apiKeySecret: 'secret' }],
|
|
41
|
-
])('when %s is missing (%j)', (field, config) => {
|
|
42
|
-
it('rejects with AMP_INVALID_CONFIG, names the field and never calls the native module', async () => {
|
|
43
|
-
const error = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
44
|
-
await expect(Amply.initialize(config)).rejects.toMatchObject({
|
|
45
|
-
code: 'AMP_INVALID_CONFIG',
|
|
46
|
-
message: expect.stringContaining(field),
|
|
47
|
-
});
|
|
48
|
-
expect(mockNativeModule.initialize).not.toHaveBeenCalled();
|
|
49
|
-
expect(error).toHaveBeenCalledWith(expect.stringContaining(field));
|
|
50
|
-
error.mockRestore();
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
it('passes the base-URL overrides through to the native layer', async () => {
|
|
54
|
-
await Amply.initialize({
|
|
55
|
-
appId: 'id',
|
|
56
|
-
apiKeyPublic: 'public',
|
|
57
|
-
apiKeySecret: 'secret',
|
|
58
|
-
configBaseUrl: 'https://config-staging.example.com',
|
|
59
|
-
backendBaseUrl: 'https://api-staging.example.com',
|
|
60
|
-
});
|
|
61
|
-
expect(mockNativeModule.initialize).toHaveBeenCalledWith(expect.objectContaining({
|
|
62
|
-
configBaseUrl: 'https://config-staging.example.com',
|
|
63
|
-
backendBaseUrl: 'https://api-staging.example.com',
|
|
64
|
-
}));
|
|
65
|
-
});
|
|
66
|
-
// `endpoint` is the older single-URL spelling. It stays accepted so existing
|
|
67
|
-
// callers keep working, but it only ever meant the backend host — the campaign
|
|
68
|
-
// manifest is served from a different one.
|
|
69
|
-
it('still accepts the legacy endpoint spelling', async () => {
|
|
70
|
-
await Amply.initialize({
|
|
71
|
-
appId: 'id',
|
|
72
|
-
apiKeyPublic: 'public',
|
|
73
|
-
apiKeySecret: 'secret',
|
|
74
|
-
endpoint: 'https://api-staging.example.com',
|
|
75
|
-
});
|
|
76
|
-
expect(mockNativeModule.initialize).toHaveBeenCalledWith(expect.objectContaining({ endpoint: 'https://api-staging.example.com' }));
|
|
77
|
-
});
|
|
78
|
-
it('tracks events through the native layer', async () => {
|
|
79
|
-
await Amply.track({ name: 'Test Event', properties: { foo: 'bar' } });
|
|
80
|
-
expect(mockNativeModule.track).toHaveBeenCalledWith({
|
|
81
|
-
name: 'Test Event',
|
|
82
|
-
properties: { foo: 'bar' },
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
it('reads recent events', async () => {
|
|
86
|
-
const events = [{ name: 'e1' }];
|
|
87
|
-
mockNativeModule.getRecentEvents.mockResolvedValueOnce(events);
|
|
88
|
-
const result = await Amply.getRecentEvents(3);
|
|
89
|
-
expect(mockNativeModule.getRecentEvents).toHaveBeenCalledWith(3);
|
|
90
|
-
expect(result).toBe(events);
|
|
91
|
-
});
|
|
92
|
-
it('requests dataset snapshots', async () => {
|
|
93
|
-
const snapshot = { foo: 'bar' };
|
|
94
|
-
mockNativeModule.getDataSetSnapshot.mockResolvedValueOnce(snapshot);
|
|
95
|
-
const type = { kind: '@device' };
|
|
96
|
-
const result = await Amply.getDataSetSnapshot(type);
|
|
97
|
-
expect(mockNativeModule.getDataSetSnapshot).toHaveBeenCalledWith(type);
|
|
98
|
-
expect(result).toBe(snapshot);
|
|
99
|
-
});
|
|
100
|
-
it.each([
|
|
101
|
-
['singleton @device', { kind: '@device' }],
|
|
102
|
-
['singleton @user', { kind: '@user' }],
|
|
103
|
-
['singleton @custom', { kind: '@custom' }],
|
|
104
|
-
['singleton @session', { kind: '@session' }],
|
|
105
|
-
[
|
|
106
|
-
'@triggeredEvent with global strategy',
|
|
107
|
-
{
|
|
108
|
-
kind: '@triggeredEvent',
|
|
109
|
-
data: {
|
|
110
|
-
countStrategy: 'global',
|
|
111
|
-
params: [{ name: 'level', value: 5 }],
|
|
112
|
-
eventName: 'LevelComplete',
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
[
|
|
117
|
-
'@triggeredEvent with session strategy and no eventName',
|
|
118
|
-
{
|
|
119
|
-
kind: '@triggeredEvent',
|
|
120
|
-
data: {
|
|
121
|
-
countStrategy: 'session',
|
|
122
|
-
params: [],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
[
|
|
127
|
-
'@events with mixed event types',
|
|
128
|
-
{
|
|
129
|
-
kind: '@events',
|
|
130
|
-
data: [
|
|
131
|
-
{ name: 'Purchase', type: 'custom', params: [{ name: 'sku', value: 'ABC' }] },
|
|
132
|
-
{ name: 'SessionStarted', type: 'system', params: [] },
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
])('passes %s to the native bridge unchanged', async (_label, type) => {
|
|
137
|
-
mockNativeModule.getDataSetSnapshot.mockResolvedValueOnce({});
|
|
138
|
-
await Amply.getDataSetSnapshot(type);
|
|
139
|
-
expect(mockNativeModule.getDataSetSnapshot).toHaveBeenCalledWith(type);
|
|
140
|
-
});
|
|
141
|
-
it('subscribes to system events through the TurboModule emitter', async () => {
|
|
142
|
-
const remove = jest.fn();
|
|
143
|
-
mockNativeModule.onSystemEvent.mockReturnValueOnce({ remove });
|
|
144
|
-
const listener = jest.fn();
|
|
145
|
-
const unsubscribe = await Amply.addSystemEventListener(listener);
|
|
146
|
-
expect(mockNativeModule.onSystemEvent).toHaveBeenCalledTimes(1);
|
|
147
|
-
// The native listener is a wrapper that filters DebugLog events
|
|
148
|
-
const nativeListener = mockNativeModule.onSystemEvent.mock.calls[0][0];
|
|
149
|
-
// DebugLog events should be filtered out
|
|
150
|
-
nativeListener({ name: 'DebugLog', type: 'log', timestamp: 1, properties: {} });
|
|
151
|
-
expect(listener).not.toHaveBeenCalled();
|
|
152
|
-
// Other events should pass through
|
|
153
|
-
const systemEvent = { name: 'SessionStart', type: 'system', timestamp: 2, properties: {} };
|
|
154
|
-
nativeListener(systemEvent);
|
|
155
|
-
expect(listener).toHaveBeenCalledWith(systemEvent);
|
|
156
|
-
unsubscribe();
|
|
157
|
-
expect(remove).toHaveBeenCalledTimes(1);
|
|
158
|
-
});
|
|
159
|
-
it('sets a single custom property via setCustomProperties', () => {
|
|
160
|
-
Amply.setCustomProperty('plan', 'premium');
|
|
161
|
-
expect(mockNativeModule.setCustomProperties).toHaveBeenCalledWith({
|
|
162
|
-
plan: 'premium',
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
it('sets multiple custom properties at once', () => {
|
|
166
|
-
Amply.setCustomProperties({ plan: 'premium', age: 25, active: true });
|
|
167
|
-
expect(mockNativeModule.setCustomProperties).toHaveBeenCalledWith({
|
|
168
|
-
plan: 'premium',
|
|
169
|
-
age: 25,
|
|
170
|
-
active: true,
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
it('gets a custom property value', async () => {
|
|
174
|
-
mockNativeModule.getCustomProperty.mockResolvedValueOnce({ value: 'premium' });
|
|
175
|
-
const result = await Amply.getCustomProperty('plan');
|
|
176
|
-
expect(mockNativeModule.getCustomProperty).toHaveBeenCalledWith('plan');
|
|
177
|
-
expect(result).toBe('premium');
|
|
178
|
-
});
|
|
179
|
-
it('returns null for a missing custom property', async () => {
|
|
180
|
-
mockNativeModule.getCustomProperty.mockResolvedValueOnce({});
|
|
181
|
-
const result = await Amply.getCustomProperty('nonexistent');
|
|
182
|
-
expect(result).toBeNull();
|
|
183
|
-
});
|
|
184
|
-
it('removes a custom property', () => {
|
|
185
|
-
Amply.removeCustomProperty('plan');
|
|
186
|
-
expect(mockNativeModule.removeCustomProperty).toHaveBeenCalledWith('plan');
|
|
187
|
-
});
|
|
188
|
-
it('clears all custom properties', () => {
|
|
189
|
-
Amply.clearCustomProperties();
|
|
190
|
-
expect(mockNativeModule.clearCustomProperties).toHaveBeenCalled();
|
|
191
|
-
});
|
|
192
|
-
it('registers deep links once and subscribes via the TurboModule emitter', async () => {
|
|
193
|
-
const remove = jest.fn();
|
|
194
|
-
mockNativeModule.onDeepLink.mockReturnValue({ remove });
|
|
195
|
-
const listener = jest.fn();
|
|
196
|
-
const unsubscribe = await Amply.addDeepLinkListener(listener);
|
|
197
|
-
expect(mockNativeModule.registerDeepLinkListener).toHaveBeenCalledTimes(1);
|
|
198
|
-
expect(mockNativeModule.onDeepLink).toHaveBeenCalledWith(listener);
|
|
199
|
-
await Amply.addDeepLinkListener(() => { });
|
|
200
|
-
expect(mockNativeModule.registerDeepLinkListener).toHaveBeenCalledTimes(1);
|
|
201
|
-
unsubscribe();
|
|
202
|
-
expect(remove).toHaveBeenCalledTimes(1);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"systemEventUtils.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/systemEventUtils.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { formatSystemEventLabel } from '../systemEventUtils';
|
|
2
|
-
function makeEvent(name, properties = {}) {
|
|
3
|
-
return { name, type: 'system', timestamp: Date.now(), properties };
|
|
4
|
-
}
|
|
5
|
-
describe('formatSystemEventLabel', () => {
|
|
6
|
-
describe('SessionStarted', () => {
|
|
7
|
-
it('returns "Session started (cold)" for cold start', () => {
|
|
8
|
-
const event = makeEvent('SessionStarted', { type: 'cold' });
|
|
9
|
-
expect(formatSystemEventLabel(event)).toBe('Session started (cold)');
|
|
10
|
-
});
|
|
11
|
-
it('returns "Session started (warm)" for warm start', () => {
|
|
12
|
-
const event = makeEvent('SessionStarted', { type: 'warm' });
|
|
13
|
-
expect(formatSystemEventLabel(event)).toBe('Session started (warm)');
|
|
14
|
-
});
|
|
15
|
-
it('returns "Session started" when type is absent', () => {
|
|
16
|
-
const event = makeEvent('SessionStarted');
|
|
17
|
-
expect(formatSystemEventLabel(event)).toBe('Session started');
|
|
18
|
-
});
|
|
19
|
-
it('handles unexpected type values gracefully', () => {
|
|
20
|
-
const event = makeEvent('SessionStarted', { type: 'unknown' });
|
|
21
|
-
expect(formatSystemEventLabel(event)).toBe('Session started (unknown)');
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
describe('other events', () => {
|
|
25
|
-
it('formats CampaignShown with campaignId and source', () => {
|
|
26
|
-
const event = makeEvent('CampaignShown', {
|
|
27
|
-
campaignId: 'abc',
|
|
28
|
-
source: 'click',
|
|
29
|
-
});
|
|
30
|
-
expect(formatSystemEventLabel(event)).toBe('Campaign shown (abc) via click');
|
|
31
|
-
});
|
|
32
|
-
it('formats EventTriggered with source event', () => {
|
|
33
|
-
const event = makeEvent('EventTriggered', { sourceEvent: 'purchase' });
|
|
34
|
-
expect(formatSystemEventLabel(event)).toBe('Event triggered (purchase)');
|
|
35
|
-
});
|
|
36
|
-
it('formats ConfigFetchFinished with campaign count', () => {
|
|
37
|
-
const event = makeEvent('ConfigFetchFinished', {
|
|
38
|
-
success: true,
|
|
39
|
-
campaignCount: 3,
|
|
40
|
-
});
|
|
41
|
-
expect(formatSystemEventLabel(event)).toBe('Config fetch finished (3 campaigns)');
|
|
42
|
-
});
|
|
43
|
-
it('formats ConfigFetchFinished failure', () => {
|
|
44
|
-
const event = makeEvent('ConfigFetchFinished', { success: false });
|
|
45
|
-
expect(formatSystemEventLabel(event)).toBe('Config fetch failed');
|
|
46
|
-
});
|
|
47
|
-
it('returns default label for SessionFinished', () => {
|
|
48
|
-
const event = makeEvent('SessionFinished');
|
|
49
|
-
expect(formatSystemEventLabel(event)).toBe('Session finished');
|
|
50
|
-
});
|
|
51
|
-
it('returns fallback for unknown events', () => {
|
|
52
|
-
const event = makeEvent('UnknownEvent');
|
|
53
|
-
expect(formatSystemEventLabel(event)).toBe('System event UnknownEvent');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
});
|