@amplytools/react-native-amply-sdk 0.15.0 → 0.15.2
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/android/build.gradle +1 -1
- 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/docs/ARCHITECTURE.md +26 -19
- package/package.json +12 -6
- package/scripts/check-package.js +159 -0
- 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 -2
- 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__/handleUrl.test.d.ts +0 -2
- package/dist/src/__tests__/handleUrl.test.d.ts.map +0 -1
- package/dist/src/__tests__/handleUrl.test.js +0 -114
- 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__/lastCampaignTouch.test.d.ts +0 -2
- package/dist/src/__tests__/lastCampaignTouch.test.d.ts.map +0 -1
- package/dist/src/__tests__/lastCampaignTouch.test.js +0 -119
- 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 -72
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/plugin/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/appLanguage.test.ts +0 -298
- package/src/__tests__/campaignResolved.test.ts +0 -294
- package/src/__tests__/gateRegistrationLifecycle.test.ts +0 -281
- package/src/__tests__/handleUrl.test.ts +0 -155
- package/src/__tests__/index.test.ts +0 -258
- package/src/__tests__/lastCampaignTouch.test.ts +0 -151
- package/src/__tests__/systemEventUtils.test.ts +0 -97
|
@@ -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
|