@amplytools/react-native-amply-sdk 0.9.1 → 0.10.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/android/build.gradle +1 -1
- package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +6 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +8 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +29 -5
- package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +4 -0
- package/android/src/newarch/jni/AmplyReactNative-generated.cpp +6 -0
- package/dist/src/__tests__/campaignResolved.test.js +16 -4
- package/dist/src/__tests__/gateRegistrationLifecycle.test.d.ts +2 -0
- package/dist/src/__tests__/gateRegistrationLifecycle.test.d.ts.map +1 -0
- package/dist/src/__tests__/gateRegistrationLifecycle.test.js +215 -0
- package/dist/src/index.d.ts +11 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +177 -38
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +9 -0
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ios/Sources/AmplyReactNative/AmplyModule.mm +65 -12
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +7 -0
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +1 -0
- package/package.json +1 -1
- package/scripts/check-spec-parity.sh +82 -0
- package/src/__tests__/campaignResolved.test.ts +23 -5
- package/src/__tests__/gateRegistrationLifecycle.test.ts +281 -0
- package/src/index.ts +193 -41
- package/src/nativeSpecs/NativeAmplyModule.ts +9 -0
package/src/index.ts
CHANGED
|
@@ -18,10 +18,101 @@ import type {
|
|
|
18
18
|
|
|
19
19
|
let deepLinkRegistered = false;
|
|
20
20
|
let debugLogListenerRegistered = false;
|
|
21
|
-
const registeredGates = new Set<string>();
|
|
22
21
|
const deepLinkSubscriptions = new Set<() => void>();
|
|
23
22
|
const campaignPresenterSubscriptions = new Set<() => void>();
|
|
24
23
|
|
|
24
|
+
/**
|
|
25
|
+
* What is registered NATIVELY, keyed by gate baseUrl — at most one entry per URL, because a
|
|
26
|
+
* second `registerGate` on the same URL evicts the first rather than stacking on top of it.
|
|
27
|
+
*
|
|
28
|
+
* This used to be a `Set<string>` written the moment a native call was made, whether or not
|
|
29
|
+
* that call did anything. Three defects came out of that one optimistic write: two presenters
|
|
30
|
+
* both showing a single presentation, a withdrawal that left the native gate live, and a
|
|
31
|
+
* registration made before `initialize` being lost for the lifetime of the process.
|
|
32
|
+
*/
|
|
33
|
+
const gateRegistrations = new Map<string, {id: number; removeSubscription: () => void}>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Identity for a gate registration. Withdrawal cannot be keyed by URL alone: an unsubscribe
|
|
37
|
+
* handed out for a registration that has since been evicted would otherwise withdraw whatever
|
|
38
|
+
* holds that URL now, killing its replacement. A stale handle must be inert.
|
|
39
|
+
*/
|
|
40
|
+
let nextGateRegistrationId = 1;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Registrations made before the SDK was up. The native side refuses those with a log line and
|
|
44
|
+
* no return value, so JS cannot tell refusal from success — it asks `isInitialized()` first and
|
|
45
|
+
* parks the work here instead, to be replayed by [initialize].
|
|
46
|
+
*/
|
|
47
|
+
type PendingRegistration = {cancelled: boolean; apply: () => void};
|
|
48
|
+
const pendingRegistrations: PendingRegistration[] = [];
|
|
49
|
+
|
|
50
|
+
/** True when the native module exists AND the SDK behind it has been initialised. */
|
|
51
|
+
function isSdkUp(): boolean {
|
|
52
|
+
try {
|
|
53
|
+
return getNativeModule().isInitialized();
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Runs [apply] now if the SDK is up, otherwise parks it until [initialize] completes.
|
|
61
|
+
* Returns a canceller so a caller that withdraws before initialisation is never replayed.
|
|
62
|
+
*/
|
|
63
|
+
function applyWhenSdkIsUp(apply: () => void): () => void {
|
|
64
|
+
if (isSdkUp()) {
|
|
65
|
+
apply();
|
|
66
|
+
return () => {};
|
|
67
|
+
}
|
|
68
|
+
const pending: PendingRegistration = {
|
|
69
|
+
cancelled: false,
|
|
70
|
+
apply: () => {
|
|
71
|
+
if (!pending.cancelled) {
|
|
72
|
+
apply();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
pendingRegistrations.push(pending);
|
|
77
|
+
return () => {
|
|
78
|
+
pending.cancelled = true;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function replayPendingRegistrations(): void {
|
|
83
|
+
const queued = pendingRegistrations.splice(0, pendingRegistrations.length);
|
|
84
|
+
queued.forEach(pending => {
|
|
85
|
+
try {
|
|
86
|
+
pending.apply();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn('[Amply] Failed to apply a registration deferred until initialize', error);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Withdraws the gate registered for [baseUrl], on both sides. A no-op when nothing is
|
|
95
|
+
* registered, so callers do not need to check first.
|
|
96
|
+
*/
|
|
97
|
+
function withdrawGate(baseUrl: string, onlyIfId?: number): void {
|
|
98
|
+
const registration = gateRegistrations.get(baseUrl);
|
|
99
|
+
if (!registration) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Compare before clearing, the same rule the SDK's own listener seams follow: a caller
|
|
103
|
+
// holding a stale handle must not withdraw the registration that replaced it.
|
|
104
|
+
if (onlyIfId !== undefined && registration.id !== onlyIfId) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
gateRegistrations.delete(baseUrl);
|
|
108
|
+
registration.removeSubscription();
|
|
109
|
+
try {
|
|
110
|
+
getNativeModule().unregisterGate(baseUrl);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn('[Amply] Failed to unregister gate', baseUrl, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
25
116
|
/**
|
|
26
117
|
* Format a debug log entry for console output using new format.
|
|
27
118
|
*/
|
|
@@ -72,12 +163,15 @@ function ensureDebugLogListener(): void {
|
|
|
72
163
|
}
|
|
73
164
|
|
|
74
165
|
async function ensureDeepLinkRegistration(): Promise<void> {
|
|
75
|
-
if (
|
|
76
|
-
|
|
166
|
+
if (deepLinkRegistered) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// The flag is set inside apply(), not here: native refuses while the SDK is down, and
|
|
170
|
+
// recording that refusal as success is what stranded the listener for the whole process.
|
|
171
|
+
applyWhenSdkIsUp(() => {
|
|
77
172
|
getNativeModule().registerDeepLinkListener();
|
|
78
173
|
deepLinkRegistered = true;
|
|
79
|
-
|
|
80
|
-
}
|
|
174
|
+
});
|
|
81
175
|
}
|
|
82
176
|
|
|
83
177
|
function trackDeepLinkSubscription(subscription?: {remove?: () => void}): () => void {
|
|
@@ -100,6 +194,8 @@ export async function initialize(config: AmplyInitializationConfig): Promise<voi
|
|
|
100
194
|
ensureDebugLogListener();
|
|
101
195
|
}
|
|
102
196
|
await getNativeModule().initialize(config);
|
|
197
|
+
// Anything the host registered before this point was parked rather than lost.
|
|
198
|
+
replayPendingRegistrations();
|
|
103
199
|
}
|
|
104
200
|
|
|
105
201
|
/**
|
|
@@ -272,7 +368,17 @@ function trackCampaignPresenterSubscription(subscription?: {remove?: () => void}
|
|
|
272
368
|
* ad lifecycles into the single result you report. A synchronous throw → `unavailable`
|
|
273
369
|
* (fail-open).
|
|
274
370
|
*
|
|
275
|
-
*
|
|
371
|
+
* One presenter per `baseUrl`: registering the same URL again REPLACES the previous
|
|
372
|
+
* presenter and re-applies the options, rather than adding a second one. Two presenters on
|
|
373
|
+
* one URL would both be handed the same presentation, so both would show UI and the outcome
|
|
374
|
+
* would be decided by whichever reported first.
|
|
375
|
+
*
|
|
376
|
+
* Safe to call before {@link initialize} — the registration is held and applied once the SDK
|
|
377
|
+
* is up. Withdrawing before then cancels it instead.
|
|
378
|
+
*
|
|
379
|
+
* @returns an unsubscribe function. It withdraws the NATIVE gate as well as this
|
|
380
|
+
* subscription; a gate left registered behind a departed presenter parks the next gated call
|
|
381
|
+
* on that URL for its whole fail-open timeout, silently.
|
|
276
382
|
*/
|
|
277
383
|
export async function registerGate(
|
|
278
384
|
baseUrl: string,
|
|
@@ -282,48 +388,88 @@ export async function registerGate(
|
|
|
282
388
|
const onAbort = opts?.onAbort ?? 'cancel';
|
|
283
389
|
const timeoutMs = opts?.timeoutMs ?? 0;
|
|
284
390
|
|
|
285
|
-
|
|
286
|
-
getNativeModule().registerGate(baseUrl, onAbort, timeoutMs);
|
|
287
|
-
registeredGates.add(baseUrl);
|
|
288
|
-
}
|
|
391
|
+
const registrationId = nextGateRegistrationId++;
|
|
289
392
|
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
393
|
+
const attach = () => {
|
|
394
|
+
// Evict at APPLY time, not at call time. Two registrations made before `initialize` are
|
|
395
|
+
// both only queued, so a call-time eviction would find nothing in the registry to evict —
|
|
396
|
+
// and both would then attach at replay, the second overwriting the map entry while the
|
|
397
|
+
// first subscription stayed live. That is the duplicate-presenter defect on another path.
|
|
398
|
+
withdrawGate(baseUrl);
|
|
298
399
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
400
|
+
const nativeModule = getNativeModule();
|
|
401
|
+
nativeModule.registerGate(baseUrl, onAbort, timeoutMs);
|
|
402
|
+
|
|
403
|
+
let subscription: {remove?: () => void} | undefined;
|
|
404
|
+
try {
|
|
405
|
+
subscription = nativeModule.onCampaignPresent((event: CampaignPresentEvent) => {
|
|
406
|
+
const {url, params, info, mediationId} = event;
|
|
407
|
+
// The native side routes by baseUrl, but guard here too because every gate shares the
|
|
408
|
+
// single onCampaignPresent channel.
|
|
409
|
+
if (baseUrl && url && !url.startsWith(baseUrl)) {
|
|
302
410
|
return;
|
|
303
411
|
}
|
|
304
|
-
settled = true;
|
|
305
|
-
nativeModule.resolveCampaign(mediationId, result);
|
|
306
|
-
};
|
|
307
|
-
const resolution: GateResolution = {
|
|
308
|
-
completed: () => report('Completed'),
|
|
309
|
-
dismissed: () => report('Dismissed'),
|
|
310
|
-
unavailable: () => report('Unavailable'),
|
|
311
|
-
};
|
|
312
412
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
413
|
+
let settled = false;
|
|
414
|
+
const report = (result: CampaignResult) => {
|
|
415
|
+
if (settled) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
settled = true;
|
|
419
|
+
nativeModule.resolveCampaign(mediationId, result);
|
|
420
|
+
};
|
|
421
|
+
const resolution: GateResolution = {
|
|
422
|
+
completed: () => report('Completed'),
|
|
423
|
+
dismissed: () => report('Dismissed'),
|
|
424
|
+
unavailable: () => report('Unavailable'),
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
presenter(
|
|
429
|
+
(params ?? {}) as Record<string, unknown>,
|
|
430
|
+
info as Record<string, unknown>,
|
|
431
|
+
resolution,
|
|
432
|
+
);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
// Presenter failure is an infra failure, never a user dismiss → Unavailable.
|
|
435
|
+
console.warn('[Amply] gate presenter threw; reporting Unavailable (fail-open)', error);
|
|
436
|
+
report('Unavailable');
|
|
437
|
+
}
|
|
438
|
+
});
|
|
319
439
|
} catch (error) {
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
440
|
+
// The native gate is already registered at this point. Leaving it would be the exact
|
|
441
|
+
// defect this whole change exists to remove — a gate with no presenter behind it — and
|
|
442
|
+
// nothing would be tracked to withdraw it later, so withdraw it here.
|
|
443
|
+
console.warn('[Amply] Failed to subscribe a gate presenter; withdrawing the gate', error);
|
|
444
|
+
try {
|
|
445
|
+
nativeModule.unregisterGate(baseUrl);
|
|
446
|
+
} catch (withdrawError) {
|
|
447
|
+
console.warn('[Amply] Failed to withdraw the gate after a failed subscribe', withdrawError);
|
|
448
|
+
}
|
|
449
|
+
throw error;
|
|
323
450
|
}
|
|
324
|
-
});
|
|
325
451
|
|
|
326
|
-
|
|
452
|
+
gateRegistrations.set(baseUrl, {
|
|
453
|
+
id: registrationId,
|
|
454
|
+
removeSubscription: () => subscription?.remove?.(),
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const cancelPending = applyWhenSdkIsUp(attach);
|
|
459
|
+
|
|
460
|
+
let withdrawn = false;
|
|
461
|
+
const unsubscribe = () => {
|
|
462
|
+
if (withdrawn) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
withdrawn = true;
|
|
466
|
+
cancelPending();
|
|
467
|
+
// Only if THIS registration still holds the URL — see withdrawGate.
|
|
468
|
+
withdrawGate(baseUrl, registrationId);
|
|
469
|
+
campaignPresenterSubscriptions.delete(unsubscribe);
|
|
470
|
+
};
|
|
471
|
+
campaignPresenterSubscriptions.add(unsubscribe);
|
|
472
|
+
return unsubscribe;
|
|
327
473
|
}
|
|
328
474
|
|
|
329
475
|
export async function getRecentEvents(limit: number): Promise<EventRecord[]> {
|
|
@@ -414,7 +560,13 @@ export function removeAllListeners(): void {
|
|
|
414
560
|
}
|
|
415
561
|
});
|
|
416
562
|
campaignPresenterSubscriptions.clear();
|
|
417
|
-
|
|
563
|
+
|
|
564
|
+
// Each unsubscribe above withdraws its own gate. Anything still here was registered by a
|
|
565
|
+
// path that did not go through registerGate's tracker; withdraw it too rather than just
|
|
566
|
+
// forgetting it, which is what the old `registeredGates.clear()` did — leaving JS believing
|
|
567
|
+
// nothing was registered while the gates were still live natively.
|
|
568
|
+
Array.from(gateRegistrations.keys()).forEach(withdrawGate);
|
|
569
|
+
pendingRegistrations.length = 0;
|
|
418
570
|
}
|
|
419
571
|
|
|
420
572
|
export type {
|
|
@@ -185,6 +185,15 @@ export interface Spec extends TurboModule {
|
|
|
185
185
|
* @param timeoutMs Max time the gate waits for a presenter result before failing open.
|
|
186
186
|
*/
|
|
187
187
|
registerGate(baseUrl: string, onAbort: string, timeoutMs: number): void;
|
|
188
|
+
/**
|
|
189
|
+
* Withdraw the gate registered for `baseUrl`. A no-op when none is registered.
|
|
190
|
+
*
|
|
191
|
+
* Required for withdrawal to mean anything: the gate registry is process-scoped, so a gate
|
|
192
|
+
* left registered by a presenter that has gone away parks the next gated call on that URL
|
|
193
|
+
* for its whole fail-open timeout — with no crash, no error and no console trace.
|
|
194
|
+
* @param baseUrl The gate action base URL passed to {@link registerGate}.
|
|
195
|
+
*/
|
|
196
|
+
unregisterGate(baseUrl: string): void;
|
|
188
197
|
getRecentEvents(limit: number): Promise<EventRecord[]>;
|
|
189
198
|
getDataSetSnapshot(type: DataSetType): Promise<DataSetSnapshot>;
|
|
190
199
|
registerDeepLinkListener(): void;
|