@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
@@ -0,0 +1,723 @@
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
+ exports.systemEvents = exports.AmplyConfigError = exports.formatSystemEventLabel = exports.useAmplySystemEvents = void 0;
7
+ exports.initialize = initialize;
8
+ exports.setAppLanguage = setAppLanguage;
9
+ exports.handleUrl = handleUrl;
10
+ exports.setUserId = setUserId;
11
+ exports.setLogLevel = setLogLevel;
12
+ exports.getLogLevel = getLogLevel;
13
+ exports.isInitialized = isInitialized;
14
+ exports.track = track;
15
+ exports.trackEvent = trackEvent;
16
+ exports.trackGated = trackGated;
17
+ exports.registerGate = registerGate;
18
+ exports.getRecentEvents = getRecentEvents;
19
+ exports.getLastCampaignTouch = getLastCampaignTouch;
20
+ exports.getDataSetSnapshot = getDataSetSnapshot;
21
+ exports.addDeepLinkListener = addDeepLinkListener;
22
+ exports.addSystemEventListener = addSystemEventListener;
23
+ exports.setCustomProperty = setCustomProperty;
24
+ exports.setCustomProperties = setCustomProperties;
25
+ exports.getCustomProperty = getCustomProperty;
26
+ exports.removeCustomProperty = removeCustomProperty;
27
+ exports.clearCustomProperties = clearCustomProperties;
28
+ exports.removeAllListeners = removeAllListeners;
29
+ exports.addSystemEventsListener = addSystemEventsListener;
30
+ const nativeModule_1 = __importDefault(require("./nativeModule"));
31
+ const systemEvents_1 = require("./systemEvents");
32
+ var useAmplySystemEvents_1 = require("./hooks/useAmplySystemEvents");
33
+ Object.defineProperty(exports, "useAmplySystemEvents", { enumerable: true, get: function () { return useAmplySystemEvents_1.useAmplySystemEvents; } });
34
+ var systemEventUtils_1 = require("./systemEventUtils");
35
+ Object.defineProperty(exports, "formatSystemEventLabel", { enumerable: true, get: function () { return systemEventUtils_1.formatSystemEventLabel; } });
36
+ const campaignTouch_1 = require("./campaignTouch");
37
+ const REQUIRED_KEYS = ['appId', 'apiKeyPublic', 'apiKeySecret'];
38
+ /** Rejection raised by {@link initialize} for a config the SDK cannot start with. */
39
+ class AmplyConfigError extends Error {
40
+ constructor(message) {
41
+ super(message);
42
+ this.code = 'AMP_INVALID_CONFIG';
43
+ this.name = 'AmplyConfigError';
44
+ }
45
+ }
46
+ exports.AmplyConfigError = AmplyConfigError;
47
+ /** The first required key that is missing or blank, if any. */
48
+ function missingKey(config) {
49
+ return REQUIRED_KEYS.find((key) => {
50
+ const value = config === null || config === void 0 ? void 0 : config[key];
51
+ return typeof value !== 'string' || value.trim().length === 0;
52
+ });
53
+ }
54
+ let deepLinkRegistered = false;
55
+ let debugLogListenerRegistered = false;
56
+ const deepLinkSubscriptions = new Set();
57
+ const campaignPresenterSubscriptions = new Set();
58
+ /**
59
+ * What is registered NATIVELY, keyed by gate baseUrl — at most one entry per URL, because a
60
+ * second `registerGate` on the same URL evicts the first rather than stacking on top of it.
61
+ *
62
+ * This used to be a `Set<string>` written the moment a native call was made, whether or not
63
+ * that call did anything. Three defects came out of that one optimistic write: two presenters
64
+ * both showing a single presentation, a withdrawal that left the native gate live, and a
65
+ * registration made before `initialize` being lost for the lifetime of the process.
66
+ */
67
+ const gateRegistrations = new Map();
68
+ /**
69
+ * Identity for a gate registration. Withdrawal cannot be keyed by URL alone: an unsubscribe
70
+ * handed out for a registration that has since been evicted would otherwise withdraw whatever
71
+ * holds that URL now, killing its replacement. A stale handle must be inert.
72
+ */
73
+ let nextGateRegistrationId = 1;
74
+ const pendingRegistrations = [];
75
+ /** True when the native module exists AND the SDK behind it has been initialised. */
76
+ function isSdkUp() {
77
+ try {
78
+ return (0, nativeModule_1.default)().isInitialized();
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ /**
85
+ * Runs [apply] now if the SDK is up, otherwise parks it until [initialize] completes.
86
+ * Returns a canceller so a caller that withdraws before initialisation is never replayed.
87
+ */
88
+ function applyWhenSdkIsUp(apply) {
89
+ if (isSdkUp()) {
90
+ apply();
91
+ return () => { };
92
+ }
93
+ const pending = {
94
+ cancelled: false,
95
+ apply: () => {
96
+ if (!pending.cancelled) {
97
+ apply();
98
+ }
99
+ },
100
+ };
101
+ pendingRegistrations.push(pending);
102
+ return () => {
103
+ pending.cancelled = true;
104
+ };
105
+ }
106
+ function replayPendingRegistrations() {
107
+ const queued = pendingRegistrations.splice(0, pendingRegistrations.length);
108
+ queued.forEach(pending => {
109
+ try {
110
+ pending.apply();
111
+ }
112
+ catch (error) {
113
+ console.warn('[Amply] Failed to apply a registration deferred until initialize', error);
114
+ }
115
+ });
116
+ }
117
+ /**
118
+ * Withdraws the gate registered for [baseUrl], on both sides. A no-op when nothing is
119
+ * registered, so callers do not need to check first.
120
+ */
121
+ function withdrawGate(baseUrl, onlyIfId) {
122
+ const registration = gateRegistrations.get(baseUrl);
123
+ if (!registration) {
124
+ return;
125
+ }
126
+ // Compare before clearing, the same rule the SDK's own listener seams follow: a caller
127
+ // holding a stale handle must not withdraw the registration that replaced it.
128
+ if (onlyIfId !== undefined && registration.id !== onlyIfId) {
129
+ return;
130
+ }
131
+ gateRegistrations.delete(baseUrl);
132
+ registration.removeSubscription();
133
+ try {
134
+ (0, nativeModule_1.default)().unregisterGate(baseUrl);
135
+ }
136
+ catch (error) {
137
+ console.warn('[Amply] Failed to unregister gate', baseUrl, error);
138
+ }
139
+ }
140
+ /**
141
+ * Format a debug log entry for console output using new format.
142
+ */
143
+ function formatDebugLog(event) {
144
+ var _a;
145
+ const props = event.properties;
146
+ const level = ((_a = props.level) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'DEBUG';
147
+ const category = props.category
148
+ ? props.category.charAt(0).toUpperCase() + props.category.slice(1)
149
+ : 'Sdk';
150
+ const timestamp = new Date(event.timestamp).toISOString();
151
+ return `[${timestamp}][Amply.${category}][${level}] ${props.message || ''}`;
152
+ }
153
+ /**
154
+ * Set up debug log listener that outputs to console.
155
+ */
156
+ function ensureDebugLogListener() {
157
+ if (debugLogListenerRegistered) {
158
+ return;
159
+ }
160
+ debugLogListenerRegistered = true;
161
+ (0, systemEvents_1.addSystemEventListenerRaw)((event) => {
162
+ var _a;
163
+ if (event.name === 'DebugLog') {
164
+ const formattedLog = formatDebugLog(event);
165
+ const props = event.properties;
166
+ const level = (_a = props.level) === null || _a === void 0 ? void 0 : _a.toLowerCase();
167
+ switch (level) {
168
+ case 'error':
169
+ console.error(formattedLog);
170
+ break;
171
+ case 'warn':
172
+ console.warn(formattedLog);
173
+ break;
174
+ case 'debug':
175
+ console.debug(formattedLog);
176
+ break;
177
+ default:
178
+ console.log(formattedLog);
179
+ }
180
+ }
181
+ });
182
+ }
183
+ async function ensureDeepLinkRegistration() {
184
+ if (deepLinkRegistered) {
185
+ return;
186
+ }
187
+ // The flag is set inside apply(), not here: native refuses while the SDK is down, and
188
+ // recording that refusal as success is what stranded the listener for the whole process.
189
+ applyWhenSdkIsUp(() => {
190
+ (0, nativeModule_1.default)().registerDeepLinkListener();
191
+ deepLinkRegistered = true;
192
+ });
193
+ }
194
+ function trackDeepLinkSubscription(subscription) {
195
+ let removed = false;
196
+ const unsubscribe = () => {
197
+ var _a;
198
+ if (removed) {
199
+ return;
200
+ }
201
+ removed = true;
202
+ (_a = subscription === null || subscription === void 0 ? void 0 : subscription.remove) === null || _a === void 0 ? void 0 : _a.call(subscription);
203
+ deepLinkSubscriptions.delete(unsubscribe);
204
+ };
205
+ deepLinkSubscriptions.add(unsubscribe);
206
+ return unsubscribe;
207
+ }
208
+ async function initialize(config) {
209
+ // Checked here, before the native module sees anything: a missing key used to reach the
210
+ // native config builder, which threw across the Objective-C boundary and aborted the host
211
+ // app. Bad keys cost the app its Amply — never its process. The SDK stays uninitialized.
212
+ const missing = missingKey(config);
213
+ if (missing) {
214
+ const message = `[Amply] '${missing}' is required — Amply is not initialized. Copy it from your application's page in Amply.`;
215
+ console.error(message);
216
+ throw new AmplyConfigError(message);
217
+ }
218
+ // Set up debug log listener if debug mode is enabled
219
+ if (config.debug || config.logLevel) {
220
+ ensureDebugLogListener();
221
+ }
222
+ // Taken now, synchronously: a call made after this point is later than the config and must
223
+ // stay parked so it is applied after it.
224
+ const parked = takeParkedAppLanguage();
225
+ try {
226
+ await (0, nativeModule_1.default)().initialize(withParkedAppLanguage(config, parked));
227
+ }
228
+ catch (error) {
229
+ // A debug link parked for a failed start has nothing to open; replaying it later would
230
+ // surprise a retry that the user did not scan for.
231
+ parkedDebugUrl = null;
232
+ // Nothing reached the SDK, so the call still has to — unless a newer one replaced it.
233
+ if (parkedAppLanguage == null && isLatestLanguageWrite(parked)) {
234
+ parkedAppLanguage = parked;
235
+ }
236
+ throw error;
237
+ }
238
+ // A parked clear cannot travel in the config, where null means "leave it alone". Replayed only
239
+ // if nothing newer came in while initialize was in flight: the native SDK reports itself up
240
+ // before this promise settles, and a call made in that window has already reached it.
241
+ if ((parked === null || parked === void 0 ? void 0 : parked.tag) === null && !hasText(config.appLanguage) && isLatestLanguageWrite(parked)) {
242
+ (0, nativeModule_1.default)().setAppLanguage(null);
243
+ }
244
+ flushParkedAppLanguage();
245
+ flushParkedDebugUrl();
246
+ // Anything the host registered before this point was parked rather than lost.
247
+ replayPendingRegistrations();
248
+ }
249
+ /**
250
+ * A {@link setAppLanguage} call made before the SDK was up. Boxed so that a parked `null` — a
251
+ * request to return to automatic detection — is distinguishable from nothing parked.
252
+ *
253
+ * The native bridges buffer such calls too, but replay them AFTER building the SDK, which applies
254
+ * the config's `appLanguage` first. An earlier call would then overwrite a later config value,
255
+ * the opposite of "last write wins". Parking it here lets {@link initialize} put the two in the
256
+ * order they were made.
257
+ */
258
+ let parkedAppLanguage = null;
259
+ /** Bumped by every {@link setAppLanguage} call, whether it is forwarded or held. */
260
+ let languageWriteSeq = 0;
261
+ function isLatestLanguageWrite(write) {
262
+ return write != null && write.seq === languageWriteSeq;
263
+ }
264
+ function hasText(value) {
265
+ return typeof value === 'string' && value.trim().length > 0;
266
+ }
267
+ function takeParkedAppLanguage() {
268
+ const parked = parkedAppLanguage;
269
+ parkedAppLanguage = null;
270
+ return parked;
271
+ }
272
+ /** The config's own appLanguage wins: initialize was called after the parked call. */
273
+ function withParkedAppLanguage(config, parked) {
274
+ if (hasText(config.appLanguage) || parked == null || parked.tag === null) {
275
+ return config;
276
+ }
277
+ return { ...config, appLanguage: parked.tag };
278
+ }
279
+ function flushParkedAppLanguage() {
280
+ const parked = takeParkedAppLanguage();
281
+ if (isLatestLanguageWrite(parked)) {
282
+ (0, nativeModule_1.default)().setAppLanguage(parked.tag);
283
+ }
284
+ }
285
+ /**
286
+ * Tell Amply the language your app's UI is in, so sheets, stories and mini-apps show in the
287
+ * matching language and campaigns can target it. Use this when the app chooses its language
288
+ * itself (an in-app language picker, i18next, …) rather than following the device.
289
+ *
290
+ * Any common tag format works — `'pt_BR'`, `'pt-BR'`, `'zh-TW'`, `'fr'`, any case. A value that
291
+ * is not a language tag is ignored by the SDK with a warning log. The value is persisted across
292
+ * launches; pass `null` to return to automatic detection. The device language is never changed.
293
+ *
294
+ * Safe to call before {@link initialize}: the value is held and applied at initialization, and
295
+ * an `appLanguage` passed to a later `initialize` call wins over it.
296
+ * @param tag The language tag, or null to clear
297
+ */
298
+ function setAppLanguage(tag) {
299
+ languageWriteSeq += 1;
300
+ if (isSdkUp()) {
301
+ (0, nativeModule_1.default)().setAppLanguage(tag);
302
+ return;
303
+ }
304
+ parkedAppLanguage = { tag, seq: languageWriteSeq };
305
+ }
306
+ /** A debug link that arrived before the SDK was up. At most one: a newer link replaces it. */
307
+ let parkedDebugUrl = null;
308
+ const URL_SCHEME_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
309
+ const DEBUG_LINK_HOST = 'amply-debug';
310
+ const DEBUG_LINK_PATH = '/campaign';
311
+ const DEBUG_LINK_TOKEN_PARAM = 't';
312
+ /**
313
+ * True for `<any scheme>://amply-debug/campaign?t=<non-empty>`. Parsed by hand, without decoding,
314
+ * so a malformed string is just "not a debug link" and can never throw.
315
+ */
316
+ function isDebugLink(url) {
317
+ const scheme = URL_SCHEME_PREFIX.exec(url);
318
+ if (scheme == null) {
319
+ return false;
320
+ }
321
+ const rest = url.slice(scheme[0].length).split('#')[0];
322
+ const queryStart = rest.indexOf('?');
323
+ if (queryStart < 0) {
324
+ return false;
325
+ }
326
+ const target = rest.slice(0, queryStart);
327
+ const slash = target.indexOf('/');
328
+ if (slash < 0 || target.slice(0, slash) !== DEBUG_LINK_HOST) {
329
+ return false;
330
+ }
331
+ if (target.slice(slash).replace(/\/+$/, '') !== DEBUG_LINK_PATH) {
332
+ return false;
333
+ }
334
+ return rest
335
+ .slice(queryStart + 1)
336
+ .split('&')
337
+ .some(pair => {
338
+ const eq = pair.indexOf('=');
339
+ return eq > 0 && pair.slice(0, eq) === DEBUG_LINK_TOKEN_PARAM && eq < pair.length - 1;
340
+ });
341
+ }
342
+ function flushParkedDebugUrl() {
343
+ var _a, _b;
344
+ const url = parkedDebugUrl;
345
+ parkedDebugUrl = null;
346
+ if (url == null) {
347
+ return;
348
+ }
349
+ try {
350
+ (_b = (_a = (0, nativeModule_1.default)()).handleUrl) === null || _b === void 0 ? void 0 : _b.call(_a, url);
351
+ }
352
+ catch (error) {
353
+ console.warn('[Amply] Failed to replay a debug link parked until initialize', error);
354
+ }
355
+ }
356
+ /**
357
+ * Hand an incoming URL to Amply. Call it for every link your app is opened with — cold start
358
+ * and while running — before your own routing; when it returns `true` Amply recognised the
359
+ * link as its own and took it over, so your app should not route it.
360
+ *
361
+ * Safe to call before {@link initialize}, which is how a cold-start link arrives: an Amply debug
362
+ * link is held (the newest one) and opened once initialization succeeds, and `true` is returned.
363
+ *
364
+ * Synchronous. Returns `false` for any other URL, for a non-string, and when the installed
365
+ * native SDK predates this method; it never throws.
366
+ * @param url The full incoming URL
367
+ */
368
+ function handleUrl(url) {
369
+ if (typeof url !== 'string') {
370
+ return false;
371
+ }
372
+ try {
373
+ if (!isSdkUp()) {
374
+ if (!isDebugLink(url)) {
375
+ return false;
376
+ }
377
+ parkedDebugUrl = url;
378
+ return true;
379
+ }
380
+ const nativeModule = (0, nativeModule_1.default)();
381
+ if (typeof nativeModule.handleUrl !== 'function') {
382
+ return false;
383
+ }
384
+ return nativeModule.handleUrl(url) === true;
385
+ }
386
+ catch {
387
+ return false;
388
+ }
389
+ }
390
+ /**
391
+ * Set the user ID for analytics. Pass null to clear.
392
+ * @param userId The user ID to set, or null to clear
393
+ */
394
+ function setUserId(userId) {
395
+ (0, nativeModule_1.default)().setUserId(userId);
396
+ }
397
+ /**
398
+ * Set the log level at runtime.
399
+ * @param level The log level: 'none' | 'error' | 'warn' | 'info' | 'debug'
400
+ */
401
+ function setLogLevel(level) {
402
+ if (level !== 'none') {
403
+ ensureDebugLogListener();
404
+ }
405
+ (0, nativeModule_1.default)().setLogLevel(level);
406
+ }
407
+ /**
408
+ * Get the current log level.
409
+ * @returns The current log level
410
+ */
411
+ function getLogLevel() {
412
+ return (0, nativeModule_1.default)().getLogLevel();
413
+ }
414
+ function isInitialized() {
415
+ return (0, nativeModule_1.default)().isInitialized();
416
+ }
417
+ /**
418
+ * Records an event. Fire-and-forget: the promise resolves once the event has been handed
419
+ * to the native SDK, not when a campaign finishes. A campaign matched by this event runs
420
+ * on its own and never blocks the caller — use {@link trackGated} at a call site that has
421
+ * to wait for a campaign's outcome before continuing.
422
+ *
423
+ * {@link trackEvent} is the same call with positional arguments.
424
+ */
425
+ async function track(payload) {
426
+ await (0, nativeModule_1.default)().track(payload);
427
+ }
428
+ /**
429
+ * Records an event — the positional-argument form of {@link track}, shaped like
430
+ * {@link trackGated} so gated and non-gated call sites read the same. Behaviour is
431
+ * identical to {@link track}: fire-and-forget, never blocks on a campaign.
432
+ *
433
+ * Both forms are supported; pick one and stay consistent within a codebase.
434
+ */
435
+ async function trackEvent(event, properties) {
436
+ await (0, nativeModule_1.default)().track({ name: event, properties: properties !== null && properties !== void 0 ? properties : {} });
437
+ }
438
+ /**
439
+ * Gated track. Records the event; if a gate action matches AND a presenter is
440
+ * registered for its `baseUrl`, the SDK dispatches the presentation and awaits its
441
+ * outcome before resolving:
442
+ * - `{ outcome: 'proceed', reason: 'completed' }` — the presenter satisfied the gate.
443
+ * - `{ outcome: 'proceed', reason: 'failOpen' }` — no gate matched, not initialized,
444
+ * timeout, unavailable, infra failure, or dismiss under `onAbort=proceed`.
445
+ * - `{ outcome: 'cancelled' }` — user dismiss under an `onAbort=cancel` gate.
446
+ *
447
+ * This NEVER rejects: any infra failure resolves to `{ outcome: 'proceed', reason:
448
+ * 'failOpen' }` so the caller is never stranded.
449
+ */
450
+ async function trackGated(event, properties) {
451
+ const props = properties !== null && properties !== void 0 ? properties : {};
452
+ try {
453
+ const decision = (await (0, nativeModule_1.default)().trackGated(event, props));
454
+ if (decision && decision.outcome === 'cancelled') {
455
+ return { outcome: 'cancelled' };
456
+ }
457
+ if (decision && decision.outcome === 'proceed') {
458
+ const reason = decision.reason === 'completed' ? 'completed' : 'failOpen';
459
+ return { outcome: 'proceed', reason };
460
+ }
461
+ // Unexpected/malformed payload: fail open.
462
+ console.warn('[Amply] trackGated returned an unexpected decision; failing open', decision);
463
+ return { outcome: 'proceed', reason: 'failOpen' };
464
+ }
465
+ catch (error) {
466
+ // Fail open: any bridge/SDK failure must proceed, never strand the caller.
467
+ console.warn('[Amply] trackGated failed; proceeding (fail-open)', error);
468
+ return { outcome: 'proceed', reason: 'failOpen' };
469
+ }
470
+ }
471
+ function trackCampaignPresenterSubscription(subscription) {
472
+ let removed = false;
473
+ const unsubscribe = () => {
474
+ var _a;
475
+ if (removed) {
476
+ return;
477
+ }
478
+ removed = true;
479
+ (_a = subscription === null || subscription === void 0 ? void 0 : subscription.remove) === null || _a === void 0 ? void 0 : _a.call(subscription);
480
+ campaignPresenterSubscriptions.delete(unsubscribe);
481
+ };
482
+ campaignPresenterSubscriptions.add(unsubscribe);
483
+ return unsubscribe;
484
+ }
485
+ /**
486
+ * Register a gate presenter for a gate (blocking) action, keyed by its `baseUrl`.
487
+ * The presenter is invoked when a matching gate is dispatched; it receives the parsed
488
+ * query `params`, the enriched `info`, and a {@link GateResolution} it must resolve
489
+ * exactly once (`completed`/`dismissed`/`unavailable`). The SDK collapses multi-step
490
+ * ad lifecycles into the single result you report. A synchronous throw → `unavailable`
491
+ * (fail-open).
492
+ *
493
+ * One presenter per `baseUrl`: registering the same URL again REPLACES the previous
494
+ * presenter and re-applies the options, rather than adding a second one. Two presenters on
495
+ * one URL would both be handed the same presentation, so both would show UI and the outcome
496
+ * would be decided by whichever reported first.
497
+ *
498
+ * Safe to call before {@link initialize} — the registration is held and applied once the SDK
499
+ * is up. Withdrawing before then cancels it instead.
500
+ *
501
+ * @returns an unsubscribe function. It withdraws the NATIVE gate as well as this
502
+ * subscription; a gate left registered behind a departed presenter parks the next gated call
503
+ * on that URL for its whole fail-open timeout, silently.
504
+ */
505
+ async function registerGate(baseUrl, presenter, opts) {
506
+ var _a, _b;
507
+ const onAbort = (_a = opts === null || opts === void 0 ? void 0 : opts.onAbort) !== null && _a !== void 0 ? _a : 'cancel';
508
+ const timeoutMs = (_b = opts === null || opts === void 0 ? void 0 : opts.timeoutMs) !== null && _b !== void 0 ? _b : 0;
509
+ const registrationId = nextGateRegistrationId++;
510
+ const attach = () => {
511
+ // Evict at APPLY time, not at call time. Two registrations made before `initialize` are
512
+ // both only queued, so a call-time eviction would find nothing in the registry to evict —
513
+ // and both would then attach at replay, the second overwriting the map entry while the
514
+ // first subscription stayed live. That is the duplicate-presenter defect on another path.
515
+ withdrawGate(baseUrl);
516
+ const nativeModule = (0, nativeModule_1.default)();
517
+ nativeModule.registerGate(baseUrl, onAbort, timeoutMs);
518
+ let subscription;
519
+ try {
520
+ subscription = nativeModule.onCampaignPresent((event) => {
521
+ const { url, params, info, mediationId } = event;
522
+ // The native side routes by baseUrl, but guard here too because every gate shares the
523
+ // single onCampaignPresent channel.
524
+ if (baseUrl && url && !url.startsWith(baseUrl)) {
525
+ return;
526
+ }
527
+ let settled = false;
528
+ const report = (result) => {
529
+ if (settled) {
530
+ return;
531
+ }
532
+ settled = true;
533
+ nativeModule.resolveCampaign(mediationId, result);
534
+ };
535
+ const resolution = {
536
+ completed: () => report('Completed'),
537
+ dismissed: () => report('Dismissed'),
538
+ unavailable: () => report('Unavailable'),
539
+ };
540
+ try {
541
+ presenter((params !== null && params !== void 0 ? params : {}), info, resolution);
542
+ }
543
+ catch (error) {
544
+ // Presenter failure is an infra failure, never a user dismiss → Unavailable.
545
+ console.warn('[Amply] gate presenter threw; reporting Unavailable (fail-open)', error);
546
+ report('Unavailable');
547
+ }
548
+ });
549
+ }
550
+ catch (error) {
551
+ // The native gate is already registered at this point. Leaving it would be the exact
552
+ // defect this whole change exists to remove — a gate with no presenter behind it — and
553
+ // nothing would be tracked to withdraw it later, so withdraw it here.
554
+ console.warn('[Amply] Failed to subscribe a gate presenter; withdrawing the gate', error);
555
+ try {
556
+ nativeModule.unregisterGate(baseUrl);
557
+ }
558
+ catch (withdrawError) {
559
+ console.warn('[Amply] Failed to withdraw the gate after a failed subscribe', withdrawError);
560
+ }
561
+ throw error;
562
+ }
563
+ gateRegistrations.set(baseUrl, {
564
+ id: registrationId,
565
+ removeSubscription: () => { var _a; return (_a = subscription === null || subscription === void 0 ? void 0 : subscription.remove) === null || _a === void 0 ? void 0 : _a.call(subscription); },
566
+ });
567
+ };
568
+ const cancelPending = applyWhenSdkIsUp(attach);
569
+ let withdrawn = false;
570
+ const unsubscribe = () => {
571
+ if (withdrawn) {
572
+ return;
573
+ }
574
+ withdrawn = true;
575
+ cancelPending();
576
+ // Only if THIS registration still holds the URL — see withdrawGate.
577
+ withdrawGate(baseUrl, registrationId);
578
+ campaignPresenterSubscriptions.delete(unsubscribe);
579
+ };
580
+ campaignPresenterSubscriptions.add(unsubscribe);
581
+ return unsubscribe;
582
+ }
583
+ async function getRecentEvents(limit) {
584
+ return (0, nativeModule_1.default)().getRecentEvents(limit);
585
+ }
586
+ /**
587
+ * The campaign the user was most recently touched by — shown, or completed — at most `withinMs`
588
+ * ago, or `null` if there was none.
589
+ *
590
+ * Use it to attribute a purchase (or any conversion) in your own analytics to the campaign that
591
+ * preceded it, including one made from somewhere other than the campaign's link:
592
+ *
593
+ * ```ts
594
+ * const touch = await Amply.getLastCampaignTouch(30 * 60 * 1000);
595
+ * analytics.track('Purchase', {sku, amply_campaign_id: touch?.campaignId});
596
+ * ```
597
+ *
598
+ * One touch per campaign, the newer of a show and a completion. `withinMs` is inclusive and
599
+ * capped at 30 days by the SDK. Resolves `null` before {@link initialize}, while the SDK's storage
600
+ * is off, and if the native call fails — it rejects only for a `withinMs` that is not a finite
601
+ * number >= 0, which is a bug at the call site.
602
+ */
603
+ async function getLastCampaignTouch(withinMs) {
604
+ (0, campaignTouch_1.assertValidWithinMs)(withinMs);
605
+ try {
606
+ return (0, campaignTouch_1.toCampaignTouch)(await (0, nativeModule_1.default)().getLastCampaignTouch(withinMs));
607
+ }
608
+ catch (error) {
609
+ // The SDK answers null for every state it can be in; a rejection here is the bridge itself
610
+ // failing, and an analytics lookup must never break the purchase flow that asked.
611
+ console.warn('[Amply] getLastCampaignTouch failed; resolving null', error);
612
+ return null;
613
+ }
614
+ }
615
+ async function getDataSetSnapshot(type) {
616
+ return (0, nativeModule_1.default)().getDataSetSnapshot(type);
617
+ }
618
+ async function addDeepLinkListener(listener) {
619
+ await ensureDeepLinkRegistration();
620
+ const subscription = (0, nativeModule_1.default)().onDeepLink(listener);
621
+ return trackDeepLinkSubscription(subscription);
622
+ }
623
+ async function addSystemEventListener(listener) {
624
+ return (0, systemEvents_1.addSystemEventListener)(listener);
625
+ }
626
+ /**
627
+ * Set a single custom property.
628
+ * @param key The property key (max 32 characters)
629
+ * @param value The property value (string max 255 characters)
630
+ */
631
+ function setCustomProperty(key, value) {
632
+ (0, nativeModule_1.default)().setCustomProperties({ [key]: value });
633
+ }
634
+ /**
635
+ * Set multiple custom properties at once.
636
+ * @param properties Key-value map of properties to set
637
+ */
638
+ function setCustomProperties(properties) {
639
+ (0, nativeModule_1.default)().setCustomProperties(properties);
640
+ }
641
+ /**
642
+ * Get a custom property value by key.
643
+ * @param key The property key
644
+ * @returns The property value, or null if not found
645
+ */
646
+ async function getCustomProperty(key) {
647
+ var _a;
648
+ const result = await (0, nativeModule_1.default)().getCustomProperty(key);
649
+ const map = result;
650
+ return (_a = map.value) !== null && _a !== void 0 ? _a : null;
651
+ }
652
+ /**
653
+ * Remove a custom property by key.
654
+ * @param key The property key to remove
655
+ */
656
+ function removeCustomProperty(key) {
657
+ (0, nativeModule_1.default)().removeCustomProperty(key);
658
+ }
659
+ /**
660
+ * Remove all custom properties.
661
+ */
662
+ function clearCustomProperties() {
663
+ (0, nativeModule_1.default)().clearCustomProperties();
664
+ }
665
+ function removeAllListeners() {
666
+ deepLinkSubscriptions.forEach(unsubscribe => {
667
+ try {
668
+ unsubscribe();
669
+ }
670
+ catch (error) {
671
+ console.warn('[Amply] Failed to remove deep link listener', error);
672
+ }
673
+ });
674
+ deepLinkSubscriptions.clear();
675
+ campaignPresenterSubscriptions.forEach(unsubscribe => {
676
+ try {
677
+ unsubscribe();
678
+ }
679
+ catch (error) {
680
+ console.warn('[Amply] Failed to remove gate presenter', error);
681
+ }
682
+ });
683
+ campaignPresenterSubscriptions.clear();
684
+ // Each unsubscribe above withdraws its own gate. Anything still here was registered by a
685
+ // path that did not go through registerGate's tracker; withdraw it too rather than just
686
+ // forgetting it, which is what the old `registeredGates.clear()` did — leaving JS believing
687
+ // nothing was registered while the gates were still live natively.
688
+ Array.from(gateRegistrations.keys()).forEach(withdrawGate);
689
+ pendingRegistrations.length = 0;
690
+ }
691
+ function addSystemEventsListener(listener) {
692
+ return addSystemEventListener(listener);
693
+ }
694
+ exports.systemEvents = {
695
+ addListener: addSystemEventsListener,
696
+ };
697
+ exports.default = {
698
+ initialize,
699
+ isInitialized,
700
+ track,
701
+ trackEvent,
702
+ trackGated,
703
+ registerGate,
704
+ getRecentEvents,
705
+ getLastCampaignTouch,
706
+ getDataSetSnapshot,
707
+ addDeepLinkListener,
708
+ addSystemEventListener,
709
+ addSystemEventsListener,
710
+ removeAllListeners,
711
+ setUserId,
712
+ setAppLanguage,
713
+ handleUrl,
714
+ setLogLevel,
715
+ getLogLevel,
716
+ setCustomProperty,
717
+ setCustomProperties,
718
+ getCustomProperty,
719
+ removeCustomProperty,
720
+ clearCustomProperties,
721
+ systemEvents: exports.systemEvents,
722
+ };
723
+ //# sourceMappingURL=index.js.map