@amplytools/react-native-amply-sdk 0.4.1 → 0.5.0

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.
@@ -11,12 +11,12 @@ const mockNativeModule = {
11
11
  initialize: jest.fn(),
12
12
  isInitialized: jest.fn(() => true),
13
13
  track: jest.fn(),
14
- trackEventGated: jest.fn(),
14
+ trackGated: jest.fn(),
15
15
  resolveCampaign: jest.fn(),
16
16
  getRecentEvents: jest.fn(),
17
17
  getDataSetSnapshot: jest.fn(),
18
18
  registerDeepLinkListener: jest.fn(),
19
- registerCampaignPresenter: jest.fn(),
19
+ registerGate: jest.fn(),
20
20
  addListener: jest.fn(),
21
21
  removeListeners: jest.fn(),
22
22
  onSystemEvent: jest.fn(),
@@ -27,97 +27,129 @@ const mockNativeModule = {
27
27
  removeCustomProperty: jest.fn(),
28
28
  clearCustomProperties: jest.fn(),
29
29
  };
30
- describe('Amply CampaignResolved present/resolve/result API', () => {
30
+ describe('Amply gate API (trackGated + registerGate)', () => {
31
31
  afterEach(() => {
32
32
  jest.clearAllMocks();
33
+ Amply.removeAllListeners();
33
34
  __setNativeModule(null);
34
35
  });
35
36
  beforeEach(() => {
36
37
  __setNativeModule(mockNativeModule);
37
38
  });
38
- describe('trackEvent continuation overload', () => {
39
- it('calls fire-and-forget track when no callbacks are supplied', async () => {
39
+ describe('trackEvent (fire-and-forget)', () => {
40
+ it('forwards to native track and never gates', async () => {
40
41
  await Amply.trackEvent('SaveTapped', { foo: 'bar' });
41
42
  expect(mockNativeModule.track).toHaveBeenCalledWith({
42
43
  name: 'SaveTapped',
43
44
  properties: { foo: 'bar' },
44
45
  });
45
- expect(mockNativeModule.trackEventGated).not.toHaveBeenCalled();
46
+ expect(mockNativeModule.trackGated).not.toHaveBeenCalled();
46
47
  });
47
- it('routes a "complete" resolution to onProceed', async () => {
48
- mockNativeModule.trackEventGated.mockResolvedValueOnce('complete');
49
- const onProceed = jest.fn();
50
- const onCancel = jest.fn();
51
- await Amply.trackEvent('SaveTapped', { foo: 'bar' }, { onProceed, onCancel });
52
- expect(mockNativeModule.trackEventGated).toHaveBeenCalledWith('SaveTapped', {
48
+ it('defaults missing properties to an empty object', async () => {
49
+ await Amply.trackEvent('Ping');
50
+ expect(mockNativeModule.track).toHaveBeenCalledWith({
51
+ name: 'Ping',
52
+ properties: {},
53
+ });
54
+ });
55
+ });
56
+ describe('trackGated', () => {
57
+ it('resolves proceed/completed when the gate is satisfied', async () => {
58
+ mockNativeModule.trackGated.mockResolvedValueOnce({
59
+ outcome: 'proceed',
60
+ reason: 'completed',
61
+ });
62
+ const decision = await Amply.trackGated('SaveTapped', { foo: 'bar' });
63
+ expect(mockNativeModule.trackGated).toHaveBeenCalledWith('SaveTapped', {
53
64
  foo: 'bar',
54
65
  });
55
- expect(onProceed).toHaveBeenCalledTimes(1);
56
- expect(onCancel).not.toHaveBeenCalled();
66
+ expect(decision).toEqual({ outcome: 'proceed', reason: 'completed' });
67
+ });
68
+ it('resolves proceed/failOpen when the native layer reports fail-open', async () => {
69
+ mockNativeModule.trackGated.mockResolvedValueOnce({
70
+ outcome: 'proceed',
71
+ reason: 'failOpen',
72
+ });
73
+ const decision = await Amply.trackGated('SaveTapped', {});
74
+ expect(decision).toEqual({ outcome: 'proceed', reason: 'failOpen' });
57
75
  });
58
- it('routes a "cancel" resolution to onCancel', async () => {
59
- mockNativeModule.trackEventGated.mockResolvedValueOnce('cancel');
60
- const onProceed = jest.fn();
61
- const onCancel = jest.fn();
62
- await Amply.trackEvent('SaveTapped', undefined, { onProceed, onCancel });
63
- expect(mockNativeModule.trackEventGated).toHaveBeenCalledWith('SaveTapped', {});
64
- expect(onCancel).toHaveBeenCalledTimes(1);
65
- expect(onProceed).not.toHaveBeenCalled();
76
+ it('resolves cancelled on a user dismiss under onAbort=cancel', async () => {
77
+ mockNativeModule.trackGated.mockResolvedValueOnce({ outcome: 'cancelled' });
78
+ const decision = await Amply.trackGated('SaveTapped', undefined);
79
+ expect(mockNativeModule.trackGated).toHaveBeenCalledWith('SaveTapped', {});
80
+ expect(decision).toEqual({ outcome: 'cancelled' });
66
81
  });
67
- it('fails open to onProceed when the native call rejects', async () => {
68
- mockNativeModule.trackEventGated.mockRejectedValueOnce(new Error('boom'));
69
- const onProceed = jest.fn();
70
- const onCancel = jest.fn();
71
- await Amply.trackEvent('SaveTapped', {}, { onProceed, onCancel });
72
- expect(onProceed).toHaveBeenCalledTimes(1);
73
- expect(onCancel).not.toHaveBeenCalled();
82
+ it('never rejects — fails open to proceed/failOpen when native rejects', async () => {
83
+ mockNativeModule.trackGated.mockRejectedValueOnce(new Error('boom'));
84
+ const decision = await Amply.trackGated('SaveTapped', {});
85
+ expect(decision).toEqual({ outcome: 'proceed', reason: 'failOpen' });
74
86
  });
75
- it('runs onProceed (not onCancel) for "cancel" when no onCancel is supplied', async () => {
76
- mockNativeModule.trackEventGated.mockResolvedValueOnce('cancel');
77
- const onProceed = jest.fn();
78
- await Amply.trackEvent('SaveTapped', {}, { onProceed });
79
- // The raw result is never exposed; a "cancel" with no onCancel hook is a no-op
80
- // for the continuation. onProceed must not fire on a cancel.
81
- expect(onProceed).not.toHaveBeenCalled();
87
+ it('fails open on an unexpected/malformed native decision', async () => {
88
+ mockNativeModule.trackGated.mockResolvedValueOnce({ outcome: 'wat' });
89
+ const decision = await Amply.trackGated('SaveTapped', {});
90
+ expect(decision).toEqual({ outcome: 'proceed', reason: 'failOpen' });
91
+ });
92
+ it('normalizes an unknown proceed reason to failOpen', async () => {
93
+ mockNativeModule.trackGated.mockResolvedValueOnce({
94
+ outcome: 'proceed',
95
+ reason: 'something-else',
96
+ });
97
+ const decision = await Amply.trackGated('SaveTapped', {});
98
+ expect(decision).toEqual({ outcome: 'proceed', reason: 'failOpen' });
82
99
  });
83
100
  });
84
- describe('registerCampaignPresenter', () => {
85
- it('registers once on the native module and subscribes to onCampaignPresent', async () => {
101
+ describe('registerGate', () => {
102
+ it('registers once on the native module per baseUrl and subscribes to onCampaignPresent', async () => {
86
103
  const remove = jest.fn();
87
104
  mockNativeModule.onCampaignPresent.mockReturnValue({ remove });
88
- const presenter = jest.fn(() => 'Completed');
89
- const unsubscribe = await Amply.registerCampaignPresenter(presenter);
90
- expect(mockNativeModule.registerCampaignPresenter).toHaveBeenCalledTimes(1);
105
+ const presenter = jest.fn();
106
+ const unsubscribe = await Amply.registerGate('stillframe://ad', presenter);
107
+ expect(mockNativeModule.registerGate).toHaveBeenCalledTimes(1);
108
+ expect(mockNativeModule.registerGate).toHaveBeenCalledWith('stillframe://ad', 'cancel', 0);
91
109
  expect(mockNativeModule.onCampaignPresent).toHaveBeenCalledTimes(1);
110
+ // Second registration of the same baseUrl re-subscribes but does NOT re-register.
111
+ await Amply.registerGate('stillframe://ad', jest.fn());
112
+ expect(mockNativeModule.registerGate).toHaveBeenCalledTimes(1);
92
113
  unsubscribe();
93
114
  expect(remove).toHaveBeenCalledTimes(1);
94
115
  });
95
- it('forwards a synchronous result to resolveCampaign', async () => {
116
+ it('passes through onAbort and timeoutMs options', async () => {
117
+ mockNativeModule.onCampaignPresent.mockReturnValue({ remove: jest.fn() });
118
+ await Amply.registerGate('stillframe://ad', jest.fn(), {
119
+ onAbort: 'proceed',
120
+ timeoutMs: 8000,
121
+ });
122
+ expect(mockNativeModule.registerGate).toHaveBeenCalledWith('stillframe://ad', 'proceed', 8000);
123
+ });
124
+ it('delivers params + info to the presenter and forwards completed', async () => {
96
125
  let emit;
97
126
  mockNativeModule.onCampaignPresent.mockImplementation((listener) => {
98
127
  emit = listener;
99
128
  return { remove: jest.fn() };
100
129
  });
101
- const presenter = jest.fn(() => 'Dismissed');
102
- await Amply.registerCampaignPresenter(presenter);
130
+ const presenter = jest.fn((_params, _info, resolution) => resolution.completed());
131
+ await Amply.registerGate('stillframe://ad', presenter);
103
132
  expect(emit).toBeDefined();
104
- emit({ url: 'stillframe://ad', info: { campaignId: 'c1' }, mediationId: 'm1' });
105
- // allow the (possibly async) presenter resolution to flush
133
+ emit({
134
+ url: 'stillframe://ad?adUnit=rewarded_main',
135
+ params: { adUnit: 'rewarded_main' },
136
+ info: { campaignId: 'c1' },
137
+ mediationId: 'm1',
138
+ });
106
139
  await flushMicrotasks();
107
- expect(presenter).toHaveBeenCalledWith('stillframe://ad', { campaignId: 'c1' });
108
- expect(mockNativeModule.resolveCampaign).toHaveBeenCalledWith('m1', 'Dismissed');
140
+ expect(presenter).toHaveBeenCalledWith({ adUnit: 'rewarded_main' }, { campaignId: 'c1' }, expect.any(Object));
141
+ expect(mockNativeModule.resolveCampaign).toHaveBeenCalledWith('m1', 'Completed');
109
142
  });
110
- it('forwards a promise-resolved result to resolveCampaign', async () => {
143
+ it('forwards a dismissed resolution', async () => {
111
144
  let emit;
112
145
  mockNativeModule.onCampaignPresent.mockImplementation((listener) => {
113
146
  emit = listener;
114
147
  return { remove: jest.fn() };
115
148
  });
116
- const presenter = jest.fn(async () => 'Unavailable');
117
- await Amply.registerCampaignPresenter(presenter);
118
- emit({ url: 'stillframe://ad', info: {}, mediationId: 'm2' });
149
+ await Amply.registerGate('stillframe://ad', (_p, _i, resolution) => resolution.dismissed());
150
+ emit({ url: 'stillframe://ad', params: {}, info: {}, mediationId: 'm2' });
119
151
  await flushMicrotasks();
120
- expect(mockNativeModule.resolveCampaign).toHaveBeenCalledWith('m2', 'Unavailable');
152
+ expect(mockNativeModule.resolveCampaign).toHaveBeenCalledWith('m2', 'Dismissed');
121
153
  });
122
154
  it('reports Unavailable (fail-open) when the presenter throws', async () => {
123
155
  let emit;
@@ -125,13 +157,40 @@ describe('Amply CampaignResolved present/resolve/result API', () => {
125
157
  emit = listener;
126
158
  return { remove: jest.fn() };
127
159
  });
128
- const presenter = jest.fn(() => {
160
+ await Amply.registerGate('stillframe://ad', () => {
129
161
  throw new Error('presenter blew up');
130
162
  });
131
- await Amply.registerCampaignPresenter(presenter);
132
- emit({ url: 'stillframe://ad', info: {}, mediationId: 'm3' });
163
+ emit({ url: 'stillframe://ad', params: {}, info: {}, mediationId: 'm3' });
133
164
  await flushMicrotasks();
134
165
  expect(mockNativeModule.resolveCampaign).toHaveBeenCalledWith('m3', 'Unavailable');
135
166
  });
167
+ it('resolves at most once even if the presenter reports twice', async () => {
168
+ let emit;
169
+ mockNativeModule.onCampaignPresent.mockImplementation((listener) => {
170
+ emit = listener;
171
+ return { remove: jest.fn() };
172
+ });
173
+ await Amply.registerGate('stillframe://ad', (_p, _i, resolution) => {
174
+ resolution.completed();
175
+ resolution.dismissed();
176
+ });
177
+ emit({ url: 'stillframe://ad', params: {}, info: {}, mediationId: 'm4' });
178
+ await flushMicrotasks();
179
+ expect(mockNativeModule.resolveCampaign).toHaveBeenCalledTimes(1);
180
+ expect(mockNativeModule.resolveCampaign).toHaveBeenCalledWith('m4', 'Completed');
181
+ });
182
+ it('ignores dispatches whose url does not match the registered baseUrl', async () => {
183
+ let emit;
184
+ mockNativeModule.onCampaignPresent.mockImplementation((listener) => {
185
+ emit = listener;
186
+ return { remove: jest.fn() };
187
+ });
188
+ const presenter = jest.fn();
189
+ await Amply.registerGate('stillframe://ad', presenter);
190
+ emit({ url: 'other://thing', params: {}, info: {}, mediationId: 'm5' });
191
+ await flushMicrotasks();
192
+ expect(presenter).not.toHaveBeenCalled();
193
+ expect(mockNativeModule.resolveCampaign).not.toHaveBeenCalled();
194
+ });
136
195
  });
137
196
  });
@@ -1,7 +1,7 @@
1
1
  export { useAmplySystemEvents } from './hooks/useAmplySystemEvents';
2
2
  export { formatSystemEventLabel } from './systemEventUtils';
3
3
  export type { FormatOptions } from './systemEventUtils';
4
- import type { CampaignResult, AmplyInitializationConfig, DataSetSnapshot, DataSetType, DeepLinkEvent, EventRecord, LogLevel, CampaignPresentEvent, TrackEventPayload } from './nativeSpecs/NativeAmplyModule';
4
+ import type { CampaignResult, GateDecision, AmplyInitializationConfig, DataSetSnapshot, DataSetType, DeepLinkEvent, EventRecord, LogLevel, CampaignPresentEvent, TrackEventPayload } from './nativeSpecs/NativeAmplyModule';
5
5
  export declare function initialize(config: AmplyInitializationConfig): Promise<void>;
6
6
  /**
7
7
  * Set the user ID for analytics. Pass null to clear.
@@ -21,58 +21,81 @@ export declare function getLogLevel(): LogLevel;
21
21
  export declare function isInitialized(): boolean;
22
22
  export declare function track(payload: TrackEventPayload): Promise<void>;
23
23
  /**
24
- * Continuation hooks for the gating form of {@link trackEvent}.
25
- *
26
- * `onProceed` is "what to do next" (run exactly once when the gate resolves in
27
- * favor of proceeding). `onCancel` is an OPTIONAL local-UI cleanup hook, invoked
28
- * only when a campaign's blocking action is user-dismissed under an `onAbort=cancel`
29
- * policy. Never put business logic in `onCancel`.
30
- *
31
- * The raw campaign result is deliberately not exposed — app code must never branch
32
- * on which action ran or how it resolved.
24
+ * Possible results a gate presenter can report. Re-exported from the
25
+ * native spec for app presenters.
26
+ */
27
+ export type { CampaignResult, GateDecision };
28
+ /**
29
+ * Outcome of a gate presenter's presentation, reported back via {@link GateResolution}.
33
30
  */
34
- export type TrackEventContinuation = {
35
- onProceed: () => void;
36
- onCancel?: () => void;
31
+ export type GatePresenterResult = CampaignResult;
32
+ /**
33
+ * Resolution token handed to a {@link GatePresenter}. The presenter MUST report
34
+ * exactly one terminal result. Convenience helpers map to the underlying
35
+ * {@link CampaignResult}.
36
+ */
37
+ export type GateResolution = {
38
+ /** The gate condition was satisfied (e.g. rewarded ad watched to completion). */
39
+ completed: () => void;
40
+ /** The user deliberately dismissed without satisfying the condition. */
41
+ dismissed: () => void;
42
+ /** Any non-user failure (no-fill, not-ready, failed-to-show). Fails open. */
43
+ unavailable: () => void;
37
44
  };
38
45
  /**
39
- * Possible results a campaign presenter can report. Re-exported from the
40
- * native spec for app presenters.
46
+ * A capability presenter for a gate (blocking) action, registered via
47
+ * {@link registerGate} and keyed by the gate's `baseUrl`. Receives the parsed query
48
+ * `params`, the enriched `info`, and a {@link GateResolution} token it must resolve
49
+ * exactly once. Throwing synchronously is treated as `unavailable` (fail-open).
41
50
  */
42
- export type { CampaignResult };
51
+ export type GatePresenter = (params: Record<string, unknown>, info: Record<string, unknown>, resolution: GateResolution) => void;
43
52
  /**
44
- * A capability presenter for a campaign present (blocking) action. Keyed by deeplink
45
- * URL semantics, NOT by event — the app is a registry of capabilities. Returns (or
46
- * resolves) the terminal {@link CampaignResult}. Throwing / rejecting is treated as
47
- * `'Unavailable'` (fail-open).
53
+ * Options for {@link registerGate}.
48
54
  */
49
- export type CampaignPresenter = (url: string, info: Record<string, unknown>) => CampaignResult | Promise<CampaignResult>;
55
+ export type RegisterGateOptions = {
56
+ /**
57
+ * Abort policy when the user dismisses the gate without satisfying it.
58
+ * - `'cancel'` → {@link trackGated} resolves `{ outcome: 'cancelled' }`.
59
+ * - `'proceed'` → {@link trackGated} resolves `{ outcome: 'proceed', reason: 'failOpen' }`.
60
+ * Defaults to `'cancel'`.
61
+ */
62
+ onAbort?: 'cancel' | 'proceed';
63
+ /**
64
+ * Max time (ms) the gate waits for a presenter result before failing open.
65
+ * Defaults to the SDK's gate default.
66
+ */
67
+ timeoutMs?: number;
68
+ };
50
69
  /**
51
70
  * Fire-and-forget analytics track. Records the event and lets any matching NORMAL
52
- * campaign action dispatch as usual. A blocking action matched here will NOT fire —
53
- * there is no continuation to gate.
71
+ * campaign action dispatch as usual. A gate action matched here will NOT block —
72
+ * there is no caller to gate. Use {@link trackGated} to await a gate.
54
73
  */
55
74
  export declare function trackEvent(event: string, properties?: Record<string, unknown> | null): Promise<void>;
56
75
  /**
57
- * Continuation (gating) form. Records the event; if a blocking campaign action
58
- * matches AND a presenter is registered, the SDK dispatches it and awaits its result
59
- * before resolving the continuation:
60
- * - proceed (completed / unavailable / timeout / dismissed+proceed) → `onProceed`
61
- * - user dismiss against an `onAbort=cancel` action → `onCancel`
76
+ * Gated track. Records the event; if a gate action matches AND a presenter is
77
+ * registered for its `baseUrl`, the SDK dispatches the presentation and awaits its
78
+ * outcome before resolving:
79
+ * - `{ outcome: 'proceed', reason: 'completed' }` — the presenter satisfied the gate.
80
+ * - `{ outcome: 'proceed', reason: 'failOpen' }` — no gate matched, not initialized,
81
+ * timeout, unavailable, infra failure, or dismiss under `onAbort=proceed`.
82
+ * - `{ outcome: 'cancelled' }` — user dismiss under an `onAbort=cancel` gate.
62
83
  *
63
- * Fails open: any error path runs `onProceed`. Passing a continuation is the gating
64
- * switch — the same event tracked elsewhere without one is a valid, non-gating call.
84
+ * This NEVER rejects: any infra failure resolves to `{ outcome: 'proceed', reason:
85
+ * 'failOpen' }` so the caller is never stranded.
65
86
  */
66
- export declare function trackEvent(event: string, properties: Record<string, unknown> | null | undefined, continuation: TrackEventContinuation): Promise<void>;
87
+ export declare function trackGated(event: string, properties?: Record<string, unknown> | null): Promise<GateDecision>;
67
88
  /**
68
- * Register a capability presenter for campaign present (blocking) actions. The presenter
69
- * is invoked when a blocking action is dispatched; it must report a terminal
70
- * {@link CampaignResult} (returned or resolved). The SDK collapses multi-step ad
71
- * lifecycles into the single result you report. Throwing/rejecting → `'Unavailable'`.
89
+ * Register a gate presenter for a gate (blocking) action, keyed by its `baseUrl`.
90
+ * The presenter is invoked when a matching gate is dispatched; it receives the parsed
91
+ * query `params`, the enriched `info`, and a {@link GateResolution} it must resolve
92
+ * exactly once (`completed`/`dismissed`/`unavailable`). The SDK collapses multi-step
93
+ * ad lifecycles into the single result you report. A synchronous throw → `unavailable`
94
+ * (fail-open).
72
95
  *
73
96
  * @returns an unsubscribe function.
74
97
  */
75
- export declare function registerCampaignPresenter(presenter: CampaignPresenter): Promise<() => void>;
98
+ export declare function registerGate(baseUrl: string, presenter: GatePresenter, opts?: RegisterGateOptions): Promise<() => void>;
76
99
  export declare function getRecentEvents(limit: number): Promise<EventRecord[]>;
77
100
  export declare function getDataSetSnapshot(type: DataSetType): Promise<DataSetSnapshot>;
78
101
  export declare function addDeepLinkListener(listener: (event: DeepLinkEvent) => void): Promise<() => void>;
@@ -118,7 +141,8 @@ declare const _default: {
118
141
  isInitialized: typeof isInitialized;
119
142
  track: typeof track;
120
143
  trackEvent: typeof trackEvent;
121
- registerCampaignPresenter: typeof registerCampaignPresenter;
144
+ trackGated: typeof trackGated;
145
+ registerGate: typeof registerGate;
122
146
  getRecentEvents: typeof getRecentEvents;
123
147
  getDataSetSnapshot: typeof getDataSetSnapshot;
124
148
  addDeepLinkListener: typeof addDeepLinkListener;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
package/dist/src/index.js CHANGED
@@ -4,7 +4,7 @@ export { useAmplySystemEvents } from './hooks/useAmplySystemEvents';
4
4
  export { formatSystemEventLabel } from './systemEventUtils';
5
5
  let deepLinkRegistered = false;
6
6
  let debugLogListenerRegistered = false;
7
- let campaignPresenterRegistered = false;
7
+ const registeredGates = new Set();
8
8
  const deepLinkSubscriptions = new Set();
9
9
  const campaignPresenterSubscriptions = new Set();
10
10
  /**
@@ -109,36 +109,45 @@ export function isInitialized() {
109
109
  export async function track(payload) {
110
110
  await getNativeModule().track(payload);
111
111
  }
112
- export async function trackEvent(event, properties, continuation) {
113
- var _a;
112
+ /**
113
+ * Fire-and-forget analytics track. Records the event and lets any matching NORMAL
114
+ * campaign action dispatch as usual. A gate action matched here will NOT block —
115
+ * there is no caller to gate. Use {@link trackGated} to await a gate.
116
+ */
117
+ export async function trackEvent(event, properties) {
118
+ await getNativeModule().track({ name: event, properties: properties !== null && properties !== void 0 ? properties : {} });
119
+ }
120
+ /**
121
+ * Gated track. Records the event; if a gate action matches AND a presenter is
122
+ * registered for its `baseUrl`, the SDK dispatches the presentation and awaits its
123
+ * outcome before resolving:
124
+ * - `{ outcome: 'proceed', reason: 'completed' }` — the presenter satisfied the gate.
125
+ * - `{ outcome: 'proceed', reason: 'failOpen' }` — no gate matched, not initialized,
126
+ * timeout, unavailable, infra failure, or dismiss under `onAbort=proceed`.
127
+ * - `{ outcome: 'cancelled' }` — user dismiss under an `onAbort=cancel` gate.
128
+ *
129
+ * This NEVER rejects: any infra failure resolves to `{ outcome: 'proceed', reason:
130
+ * 'failOpen' }` so the caller is never stranded.
131
+ */
132
+ export async function trackGated(event, properties) {
114
133
  const props = properties !== null && properties !== void 0 ? properties : {};
115
- if (!continuation) {
116
- await getNativeModule().track({ name: event, properties: props });
117
- return;
118
- }
119
- let decision;
120
134
  try {
121
- decision = await getNativeModule().trackEventGated(event, props);
135
+ const decision = (await getNativeModule().trackGated(event, props));
136
+ if (decision && decision.outcome === 'cancelled') {
137
+ return { outcome: 'cancelled' };
138
+ }
139
+ if (decision && decision.outcome === 'proceed') {
140
+ const reason = decision.reason === 'completed' ? 'completed' : 'failOpen';
141
+ return { outcome: 'proceed', reason };
142
+ }
143
+ // Unexpected/malformed payload: fail open.
144
+ console.warn('[Amply] trackGated returned an unexpected decision; failing open', decision);
145
+ return { outcome: 'proceed', reason: 'failOpen' };
122
146
  }
123
147
  catch (error) {
124
- // Fail open: any bridge/SDK failure must proceed, never strand the app.
125
- console.warn('[Amply] trackEvent continuation failed; proceeding (fail-open)', error);
126
- continuation.onProceed();
127
- return;
128
- }
129
- if (decision === 'cancel') {
130
- // A deliberate user-dismiss under an onAbort=cancel action. Only the local-UI
131
- // cleanup hook runs; the continuation is suppressed.
132
- (_a = continuation.onCancel) === null || _a === void 0 ? void 0 : _a.call(continuation);
133
- return;
134
- }
135
- // 'complete' and any unexpected value fail open to onProceed.
136
- continuation.onProceed();
137
- }
138
- async function ensureCampaignPresenterRegistration() {
139
- if (!campaignPresenterRegistered) {
140
- getNativeModule().registerCampaignPresenter();
141
- campaignPresenterRegistered = true;
148
+ // Fail open: any bridge/SDK failure must proceed, never strand the caller.
149
+ console.warn('[Amply] trackGated failed; proceeding (fail-open)', error);
150
+ return { outcome: 'proceed', reason: 'failOpen' };
142
151
  }
143
152
  }
144
153
  function trackCampaignPresenterSubscription(subscription) {
@@ -156,27 +165,52 @@ function trackCampaignPresenterSubscription(subscription) {
156
165
  return unsubscribe;
157
166
  }
158
167
  /**
159
- * Register a capability presenter for campaign present (blocking) actions. The presenter
160
- * is invoked when a blocking action is dispatched; it must report a terminal
161
- * {@link CampaignResult} (returned or resolved). The SDK collapses multi-step ad
162
- * lifecycles into the single result you report. Throwing/rejecting → `'Unavailable'`.
168
+ * Register a gate presenter for a gate (blocking) action, keyed by its `baseUrl`.
169
+ * The presenter is invoked when a matching gate is dispatched; it receives the parsed
170
+ * query `params`, the enriched `info`, and a {@link GateResolution} it must resolve
171
+ * exactly once (`completed`/`dismissed`/`unavailable`). The SDK collapses multi-step
172
+ * ad lifecycles into the single result you report. A synchronous throw → `unavailable`
173
+ * (fail-open).
163
174
  *
164
175
  * @returns an unsubscribe function.
165
176
  */
166
- export async function registerCampaignPresenter(presenter) {
167
- await ensureCampaignPresenterRegistration();
177
+ export async function registerGate(baseUrl, presenter, opts) {
178
+ var _a, _b;
179
+ const onAbort = (_a = opts === null || opts === void 0 ? void 0 : opts.onAbort) !== null && _a !== void 0 ? _a : 'cancel';
180
+ const timeoutMs = (_b = opts === null || opts === void 0 ? void 0 : opts.timeoutMs) !== null && _b !== void 0 ? _b : 0;
181
+ if (!registeredGates.has(baseUrl)) {
182
+ getNativeModule().registerGate(baseUrl, onAbort, timeoutMs);
183
+ registeredGates.add(baseUrl);
184
+ }
168
185
  const nativeModule = getNativeModule();
169
186
  const subscription = nativeModule.onCampaignPresent((event) => {
170
- const { url, info, mediationId } = event;
171
- void Promise.resolve()
172
- .then(() => presenter(url, info))
173
- .then(result => {
187
+ const { url, params, info, mediationId } = event;
188
+ // The native side routes by baseUrl, but guard here too in case multiple gates
189
+ // share the single onCampaignPresent channel.
190
+ if (baseUrl && url && !url.startsWith(baseUrl)) {
191
+ return;
192
+ }
193
+ let settled = false;
194
+ const report = (result) => {
195
+ if (settled) {
196
+ return;
197
+ }
198
+ settled = true;
174
199
  nativeModule.resolveCampaign(mediationId, result);
175
- }, error => {
200
+ };
201
+ const resolution = {
202
+ completed: () => report('Completed'),
203
+ dismissed: () => report('Dismissed'),
204
+ unavailable: () => report('Unavailable'),
205
+ };
206
+ try {
207
+ presenter((params !== null && params !== void 0 ? params : {}), info, resolution);
208
+ }
209
+ catch (error) {
176
210
  // Presenter failure is an infra failure, never a user dismiss → Unavailable.
177
- console.warn('[Amply] campaign presenter failed; reporting Unavailable', error);
178
- nativeModule.resolveCampaign(mediationId, 'Unavailable');
179
- });
211
+ console.warn('[Amply] gate presenter threw; reporting Unavailable (fail-open)', error);
212
+ report('Unavailable');
213
+ }
180
214
  });
181
215
  return trackCampaignPresenterSubscription(subscription);
182
216
  }
@@ -248,10 +282,11 @@ export function removeAllListeners() {
248
282
  unsubscribe();
249
283
  }
250
284
  catch (error) {
251
- console.warn('[Amply] Failed to remove campaign presenter', error);
285
+ console.warn('[Amply] Failed to remove gate presenter', error);
252
286
  }
253
287
  });
254
288
  campaignPresenterSubscriptions.clear();
289
+ registeredGates.clear();
255
290
  }
256
291
  export function addSystemEventsListener(listener) {
257
292
  return addSystemEventListener(listener);
@@ -264,7 +299,8 @@ export default {
264
299
  isInitialized,
265
300
  track,
266
301
  trackEvent,
267
- registerCampaignPresenter,
302
+ trackGated,
303
+ registerGate,
268
304
  getRecentEvents,
269
305
  getDataSetSnapshot,
270
306
  addDeepLinkListener,