@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.
Files changed (26) hide show
  1. package/AmplyReactNative.podspec +1 -1
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +6 -0
  4. package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +8 -0
  5. package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +29 -5
  6. package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +4 -0
  7. package/android/src/newarch/jni/AmplyReactNative-generated.cpp +6 -0
  8. package/dist/src/__tests__/campaignResolved.test.js +16 -4
  9. package/dist/src/__tests__/gateRegistrationLifecycle.test.d.ts +2 -0
  10. package/dist/src/__tests__/gateRegistrationLifecycle.test.d.ts.map +1 -0
  11. package/dist/src/__tests__/gateRegistrationLifecycle.test.js +215 -0
  12. package/dist/src/index.d.ts +11 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +177 -38
  15. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +9 -0
  16. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -1
  17. package/dist/tsconfig.tsbuildinfo +1 -1
  18. package/ios/Sources/AmplyReactNative/AmplyModule.mm +65 -12
  19. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +7 -0
  20. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +1 -0
  21. package/package.json +1 -1
  22. package/scripts/check-spec-parity.sh +82 -0
  23. package/src/__tests__/campaignResolved.test.ts +23 -5
  24. package/src/__tests__/gateRegistrationLifecycle.test.ts +281 -0
  25. package/src/index.ts +193 -41
  26. package/src/nativeSpecs/NativeAmplyModule.ts +9 -0
@@ -72,6 +72,10 @@ namespace facebook::react {
72
72
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "registerGate", @selector(registerGate:onAbort:timeoutMs:), args, count);
73
73
  }
74
74
 
75
+ static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_unregisterGate(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
76
+ return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "unregisterGate", @selector(unregisterGate:), args, count);
77
+ }
78
+
75
79
  static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_getRecentEvents(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
76
80
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "getRecentEvents", @selector(getRecentEvents:resolve:reject:), args, count);
77
81
  }
@@ -141,6 +145,9 @@ namespace facebook::react {
141
145
  methodMap_["registerGate"] = MethodMetadata {3, __hostFunction_NativeAmplyModuleSpecJSI_registerGate};
142
146
 
143
147
 
148
+ methodMap_["unregisterGate"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_unregisterGate};
149
+
150
+
144
151
  methodMap_["getRecentEvents"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_getRecentEvents};
145
152
 
146
153
 
@@ -89,6 +89,7 @@ namespace JS {
89
89
  - (void)registerGate:(NSString *)baseUrl
90
90
  onAbort:(NSString *)onAbort
91
91
  timeoutMs:(double)timeoutMs;
92
+ - (void)unregisterGate:(NSString *)baseUrl;
92
93
  - (void)getRecentEvents:(double)limit
93
94
  resolve:(RCTPromiseResolveBlock)resolve
94
95
  reject:(RCTPromiseRejectBlock)reject;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amplytools/react-native-amply-sdk",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
4
4
  "description": "React Native SDK for Amply: Mobile SDK to orchestrate in-app experiences and campaigns remotely, without app releases",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Every method on the TS `Spec` interface must also exist in the three hand-written native
4
+ # specs, or JS calls it and nothing happens.
5
+ #
6
+ # This repo has no codegenConfig: the TurboModule specs are written by hand and committed.
7
+ # Android fails loudly when they drift — the Kotlin module stops overriding an abstract method
8
+ # and the build breaks. iOS does not fail at all. The .mm implementation compiles fine on its
9
+ # own; the method simply never reaches the JSI method map, so the JS call is a silent no-op.
10
+ #
11
+ # That is how `unregisterGate` shipped half-wired in the first pass of the gate-lifecycle work:
12
+ # TS spec, Android spec, both implementations, a clean clang syntax check — and an iOS binding
13
+ # that did not exist. Caught in review, not by any check. This is that check.
14
+ #
15
+ # Usage: bash scripts/check-spec-parity.sh
16
+ set -uo pipefail
17
+
18
+ cd "$(dirname "$0")/.."
19
+
20
+ TS_SPEC="src/nativeSpecs/NativeAmplyModule.ts"
21
+ ANDROID_SPEC="android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java"
22
+ ANDROID_JNI="android/src/newarch/jni/AmplyReactNative-generated.cpp"
23
+ IOS_HEADER="ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h"
24
+ IOS_GENERATED="ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm"
25
+
26
+ for f in "$TS_SPEC" "$ANDROID_SPEC" "$ANDROID_JNI" "$IOS_HEADER" "$IOS_GENERATED"; do
27
+ if [ ! -f "$f" ]; then
28
+ echo "check-spec-parity: FAIL — missing $f" >&2
29
+ exit 1
30
+ fi
31
+ done
32
+
33
+ # Method names from the TS `Spec` interface body: lines like ` name(args): Ret;` at one level
34
+ # of indentation, excluding comments and the interface line itself.
35
+ methods=$(
36
+ awk '/^export interface Spec extends TurboModule \{/{inside=1; next}
37
+ inside && /^\}/{inside=0}
38
+ inside' "$TS_SPEC" |
39
+ grep -oE '^ [a-zA-Z][a-zA-Z0-9_]*\(' |
40
+ tr -d ' (' |
41
+ sort -u
42
+ )
43
+
44
+ if [ -z "$methods" ]; then
45
+ echo "check-spec-parity: FAIL — parsed zero methods from $TS_SPEC; the parser is broken" >&2
46
+ exit 1
47
+ fi
48
+
49
+ fail=0
50
+ for m in $methods; do
51
+ # addListener/removeListeners are the RN event-emitter contract, supplied by the base class
52
+ # on iOS rather than declared per-module.
53
+ case "$m" in
54
+ addListener|removeListeners) continue ;;
55
+ esac
56
+
57
+ grep -q "abstract .*[ *]$m(" "$ANDROID_SPEC" || {
58
+ echo "check-spec-parity: $m is in the TS spec but NOT abstract in $ANDROID_SPEC" >&2
59
+ fail=1
60
+ }
61
+ grep -qE "^- \(.*\)$m[:;]" "$IOS_HEADER" || {
62
+ echo "check-spec-parity: $m is in the TS spec but NOT declared in $IOS_HEADER" >&2
63
+ fail=1
64
+ }
65
+ # The method maps are what actually decide whether JS can reach it. BOTH platforms have one,
66
+ # and both are hand-maintained here — Android's Java abstract spec is not enough on its own.
67
+ for map in "$IOS_GENERATED" "$ANDROID_JNI"; do
68
+ grep -q "methodMap_\[\"$m\"\]" "$map" || {
69
+ echo "check-spec-parity: $m is in the TS spec but NOT in the JSI methodMap_ in $map" >&2
70
+ echo " → JS would call it and nothing would happen, silently." >&2
71
+ fail=1
72
+ }
73
+ done
74
+ done
75
+
76
+ if [ "$fail" -ne 0 ]; then
77
+ echo "check-spec-parity: FAIL" >&2
78
+ exit 1
79
+ fi
80
+
81
+ count=$(echo "$methods" | wc -w | tr -d ' ')
82
+ echo "check-spec-parity: OK — all $count TS spec methods are wired on both platforms."
@@ -26,6 +26,7 @@ const mockNativeModule = {
26
26
  getDataSetSnapshot: jest.fn(),
27
27
  registerDeepLinkListener: jest.fn(),
28
28
  registerGate: jest.fn(),
29
+ unregisterGate: jest.fn(),
29
30
  addListener: jest.fn(),
30
31
  removeListeners: jest.fn(),
31
32
  onSystemEvent: jest.fn(),
@@ -133,7 +134,7 @@ describe('Amply gate API (trackGated + registerGate)', () => {
133
134
  });
134
135
 
135
136
  describe('registerGate', () => {
136
- it('registers once on the native module per baseUrl and subscribes to onCampaignPresent', async () => {
137
+ it('registers on the native module and subscribes to onCampaignPresent', async () => {
137
138
  const remove = jest.fn();
138
139
  mockNativeModule.onCampaignPresent.mockReturnValue({remove});
139
140
 
@@ -144,12 +145,29 @@ describe('Amply gate API (trackGated + registerGate)', () => {
144
145
  expect(mockNativeModule.registerGate).toHaveBeenCalledWith('stillframe://ad', 'cancel', 0);
145
146
  expect(mockNativeModule.onCampaignPresent).toHaveBeenCalledTimes(1);
146
147
 
147
- // Second registration of the same baseUrl re-subscribes but does NOT re-register.
148
- await Amply.registerGate('stillframe://ad', jest.fn());
149
- expect(mockNativeModule.registerGate).toHaveBeenCalledTimes(1);
150
-
151
148
  unsubscribe();
152
149
  expect(remove).toHaveBeenCalledTimes(1);
150
+ expect(mockNativeModule.unregisterGate).toHaveBeenCalledWith('stillframe://ad');
151
+ });
152
+
153
+ // This used to assert the opposite — that a second registration re-subscribed but did
154
+ // NOT re-register natively. That encoded the defect: the second subscriber joined the
155
+ // shared onCampaignPresent channel while the first stayed on it, so one presentation was
156
+ // handed to both presenters, and the second call's onAbort/timeout were silently dropped.
157
+ // See https://trello.com/c/Gq589zwz; the full behaviour is covered in
158
+ // gateRegistrationLifecycle.test.ts.
159
+ it('replaces the previous registration when the same baseUrl is registered again', async () => {
160
+ mockNativeModule.onCampaignPresent.mockReturnValue({remove: jest.fn()});
161
+
162
+ await Amply.registerGate('stillframe://ad', jest.fn());
163
+ await Amply.registerGate('stillframe://ad', jest.fn(), {timeoutMs: 5000});
164
+
165
+ expect(mockNativeModule.unregisterGate).toHaveBeenCalledWith('stillframe://ad');
166
+ expect(mockNativeModule.registerGate).toHaveBeenLastCalledWith(
167
+ 'stillframe://ad',
168
+ 'cancel',
169
+ 5000,
170
+ );
153
171
  });
154
172
 
155
173
  it('passes through onAbort and timeoutMs options', async () => {
@@ -0,0 +1,281 @@
1
+ import * as Amply from '../index';
2
+ import {__setNativeModule} from '../nativeModule';
3
+
4
+ type CampaignPresentEvent = {
5
+ url: string;
6
+ params: Record<string, unknown>;
7
+ info: Record<string, unknown>;
8
+ mediationId: string;
9
+ };
10
+
11
+ async function flushMicrotasks(): Promise<void> {
12
+ for (let i = 0; i < 5; i++) {
13
+ await Promise.resolve();
14
+ }
15
+ }
16
+
17
+ /**
18
+ * The registration lifecycle of a gate: who receives a presentation, what withdrawal
19
+ * actually withdraws, and what happens to a registration made before the SDK is up.
20
+ *
21
+ * All three were found on 2026-07-31 — the first two by reading the bridge after a field
22
+ * report on Happens, the third by the field report itself. They share one cause: the JS side
23
+ * keeps its own record of what is registered natively, and that record was written
24
+ * optimistically and never reconciled.
25
+ *
26
+ * Trello: gate eviction https://trello.com/c/Gq589zwz, withdrawal
27
+ * https://trello.com/c/saf8I9c6, pre-init registration https://trello.com/c/TZCixnYi
28
+ */
29
+ describe('gate registration lifecycle', () => {
30
+ /** Every listener currently attached to the shared onCampaignPresent channel. */
31
+ let emitters: Array<(e: CampaignPresentEvent) => void>;
32
+
33
+ const mockNativeModule = {
34
+ initialize: jest.fn(),
35
+ isInitialized: jest.fn(() => true),
36
+ track: jest.fn(),
37
+ trackGated: jest.fn(),
38
+ resolveCampaign: jest.fn(),
39
+ getRecentEvents: jest.fn(),
40
+ getDataSetSnapshot: jest.fn(),
41
+ registerDeepLinkListener: jest.fn(),
42
+ registerGate: jest.fn(),
43
+ unregisterGate: jest.fn(),
44
+ addListener: jest.fn(),
45
+ removeListeners: jest.fn(),
46
+ onSystemEvent: jest.fn(),
47
+ onDeepLink: jest.fn(),
48
+ onCampaignPresent: jest.fn(),
49
+ setCustomProperties: jest.fn(),
50
+ getCustomProperty: jest.fn(),
51
+ removeCustomProperty: jest.fn(),
52
+ clearCustomProperties: jest.fn(),
53
+ };
54
+
55
+ beforeEach(() => {
56
+ emitters = [];
57
+ mockNativeModule.isInitialized.mockReturnValue(true);
58
+ mockNativeModule.onCampaignPresent.mockImplementation(
59
+ (listener: (e: CampaignPresentEvent) => void) => {
60
+ emitters.push(listener);
61
+ // `remove` must actually detach, as a real RN subscription does — otherwise this
62
+ // harness reports a listener as live after it was removed, and every eviction and
63
+ // withdrawal assertion below would be measuring the mock rather than the code.
64
+ const remove = jest.fn(() => {
65
+ const at = emitters.indexOf(listener);
66
+ if (at >= 0) {
67
+ emitters.splice(at, 1);
68
+ }
69
+ });
70
+ return {remove};
71
+ },
72
+ );
73
+ mockNativeModule.onDeepLink.mockReturnValue({remove: jest.fn()});
74
+ __setNativeModule(mockNativeModule as never);
75
+ });
76
+
77
+ afterEach(() => {
78
+ Amply.removeAllListeners();
79
+ jest.clearAllMocks();
80
+ __setNativeModule(null);
81
+ });
82
+
83
+ /** Delivers one presentation to every listener currently subscribed to the channel. */
84
+ function emitPresentation(mediationId = 'm1'): void {
85
+ const event: CampaignPresentEvent = {
86
+ url: 'stillframe://ad?adUnit=main',
87
+ params: {adUnit: 'main'},
88
+ info: {campaignId: 'c1'},
89
+ mediationId,
90
+ };
91
+ emitters.forEach(emit => emit(event));
92
+ }
93
+
94
+ describe('a second registration on the same baseUrl (Gq589zwz)', () => {
95
+ it('leaves exactly one live presenter, not two', async () => {
96
+ const first = jest.fn((_p, _i, r: Amply.GateResolution) => r.completed());
97
+ const second = jest.fn((_p, _i, r: Amply.GateResolution) => r.completed());
98
+
99
+ await Amply.registerGate('stillframe://ad', first);
100
+ await Amply.registerGate('stillframe://ad', second);
101
+
102
+ emitPresentation();
103
+ await flushMicrotasks();
104
+
105
+ // Both presenters showing UI for one presentation is the defect: the outcome is then
106
+ // decided by whichever reported first, and the loser's UI is orphaned on screen.
107
+ expect(second).toHaveBeenCalledTimes(1);
108
+ expect(first).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it('applies the options of the newest registration', async () => {
112
+ await Amply.registerGate('stillframe://ad', jest.fn(), {
113
+ onAbort: 'cancel',
114
+ timeoutMs: 1000,
115
+ });
116
+ await Amply.registerGate('stillframe://ad', jest.fn(), {
117
+ onAbort: 'proceed',
118
+ timeoutMs: 8000,
119
+ });
120
+
121
+ // Previously the second call was a no-op natively, so its onAbort and timeout were
122
+ // silently discarded and the gate kept running under the first call's policy.
123
+ expect(mockNativeModule.registerGate).toHaveBeenLastCalledWith(
124
+ 'stillframe://ad',
125
+ 'proceed',
126
+ 8000,
127
+ );
128
+ });
129
+
130
+ it('keeps gates on different baseUrls independent', async () => {
131
+ const ad = jest.fn((_p, _i, r: Amply.GateResolution) => r.completed());
132
+ const paywall = jest.fn((_p, _i, r: Amply.GateResolution) => r.completed());
133
+
134
+ await Amply.registerGate('stillframe://ad', ad);
135
+ await Amply.registerGate('stillframe://paywall', paywall);
136
+
137
+ emitPresentation();
138
+ await flushMicrotasks();
139
+
140
+ // Only the URL-matching presenter runs; evicting by baseUrl must not evict a sibling.
141
+ expect(ad).toHaveBeenCalledTimes(1);
142
+ expect(paywall).not.toHaveBeenCalled();
143
+ });
144
+ });
145
+
146
+ describe('withdrawal (saf8I9c6)', () => {
147
+ it('withdraws the native gate, not just the JS subscription', async () => {
148
+ const unsubscribe = await Amply.registerGate('stillframe://ad', jest.fn());
149
+
150
+ unsubscribe();
151
+
152
+ // Without this the gate outlives its presenter and parks the next gated call on that
153
+ // URL for the whole fail-open timeout — silently, with no crash and no console trace.
154
+ expect(mockNativeModule.unregisterGate).toHaveBeenCalledWith('stillframe://ad');
155
+ });
156
+
157
+ it('lets the same baseUrl be registered again after withdrawal', async () => {
158
+ const unsubscribe = await Amply.registerGate('stillframe://ad', jest.fn());
159
+ unsubscribe();
160
+
161
+ const presenter = jest.fn((_p, _i, r: Amply.GateResolution) => r.completed());
162
+ await Amply.registerGate('stillframe://ad', presenter);
163
+
164
+ emitPresentation();
165
+ await flushMicrotasks();
166
+
167
+ expect(presenter).toHaveBeenCalledTimes(1);
168
+ });
169
+
170
+ /**
171
+ * Found by codex, not by me. Withdrawal was keyed by URL alone, so an unsubscribe handed
172
+ * out for a registration that has since been evicted still withdrew whatever holds that
173
+ * URL now — killing its replacement. A stale handle must be inert, not destructive.
174
+ */
175
+ it('an evicted registration\'s unsubscribe does not withdraw its replacement', async () => {
176
+ const withdrawFirst = await Amply.registerGate('stillframe://ad', jest.fn());
177
+
178
+ const second = jest.fn((_p, _i, r: Amply.GateResolution) => r.completed());
179
+ await Amply.registerGate('stillframe://ad', second);
180
+ mockNativeModule.unregisterGate.mockClear();
181
+
182
+ withdrawFirst();
183
+
184
+ expect(mockNativeModule.unregisterGate).not.toHaveBeenCalled();
185
+
186
+ emitPresentation();
187
+ await flushMicrotasks();
188
+ expect(second).toHaveBeenCalledTimes(1);
189
+ });
190
+
191
+ it('is idempotent — a second call withdraws nothing further', async () => {
192
+ const unsubscribe = await Amply.registerGate('stillframe://ad', jest.fn());
193
+
194
+ unsubscribe();
195
+ unsubscribe();
196
+
197
+ expect(mockNativeModule.unregisterGate).toHaveBeenCalledTimes(1);
198
+ });
199
+
200
+ it('removeAllListeners withdraws every native gate too', async () => {
201
+ await Amply.registerGate('stillframe://ad', jest.fn());
202
+ await Amply.registerGate('stillframe://paywall', jest.fn());
203
+
204
+ Amply.removeAllListeners();
205
+
206
+ // It used to clear the JS bookkeeping only, leaving JS believing nothing was
207
+ // registered while both gates were still live natively.
208
+ expect(mockNativeModule.unregisterGate).toHaveBeenCalledWith('stillframe://ad');
209
+ expect(mockNativeModule.unregisterGate).toHaveBeenCalledWith('stillframe://paywall');
210
+ });
211
+ });
212
+
213
+ describe('registration before initialize (TZCixnYi)', () => {
214
+ it('replays a gate registration once the SDK is up', async () => {
215
+ mockNativeModule.isInitialized.mockReturnValue(false);
216
+
217
+ const presenter = jest.fn((_p, _i, r: Amply.GateResolution) => r.completed());
218
+ await Amply.registerGate('stillframe://ad', presenter);
219
+
220
+ // Native refuses while uninitialised; JS must not record that as done.
221
+ expect(mockNativeModule.registerGate).not.toHaveBeenCalled();
222
+
223
+ mockNativeModule.isInitialized.mockReturnValue(true);
224
+ await Amply.initialize({apiKey: 'k', apiKeySecret: 's'} as never);
225
+
226
+ expect(mockNativeModule.registerGate).toHaveBeenCalledWith('stillframe://ad', 'cancel', 0);
227
+
228
+ emitPresentation();
229
+ await flushMicrotasks();
230
+ expect(presenter).toHaveBeenCalledTimes(1);
231
+ });
232
+
233
+ it('replays a deeplink listener once the SDK is up', async () => {
234
+ mockNativeModule.isInitialized.mockReturnValue(false);
235
+
236
+ await Amply.addDeepLinkListener(jest.fn());
237
+ expect(mockNativeModule.registerDeepLinkListener).not.toHaveBeenCalled();
238
+
239
+ mockNativeModule.isInitialized.mockReturnValue(true);
240
+ await Amply.initialize({apiKey: 'k', apiKeySecret: 's'} as never);
241
+
242
+ expect(mockNativeModule.registerDeepLinkListener).toHaveBeenCalledTimes(1);
243
+ });
244
+
245
+ /**
246
+ * Found by self-review, not by the original pass. Eviction ran at `registerGate()` time,
247
+ * but a queued registration is not in the registry yet, so there was nothing to evict —
248
+ * both attached at replay and the second overwrote the map entry without removing the
249
+ * first subscription. That is the duplicate-presenter defect again, on the pre-init path.
250
+ */
251
+ it('two registrations on one baseUrl before init still leave one live presenter', async () => {
252
+ mockNativeModule.isInitialized.mockReturnValue(false);
253
+
254
+ const first = jest.fn((_p, _i, r: Amply.GateResolution) => r.completed());
255
+ const second = jest.fn((_p, _i, r: Amply.GateResolution) => r.completed());
256
+ await Amply.registerGate('stillframe://ad', first);
257
+ await Amply.registerGate('stillframe://ad', second);
258
+
259
+ mockNativeModule.isInitialized.mockReturnValue(true);
260
+ await Amply.initialize({apiKey: 'k', apiKeySecret: 's'} as never);
261
+
262
+ emitPresentation();
263
+ await flushMicrotasks();
264
+
265
+ expect(second).toHaveBeenCalledTimes(1);
266
+ expect(first).not.toHaveBeenCalled();
267
+ });
268
+
269
+ it('withdrawing a queued gate cancels it instead of replaying it', async () => {
270
+ mockNativeModule.isInitialized.mockReturnValue(false);
271
+
272
+ const unsubscribe = await Amply.registerGate('stillframe://ad', jest.fn());
273
+ unsubscribe();
274
+
275
+ mockNativeModule.isInitialized.mockReturnValue(true);
276
+ await Amply.initialize({apiKey: 'k', apiKeySecret: 's'} as never);
277
+
278
+ expect(mockNativeModule.registerGate).not.toHaveBeenCalled();
279
+ });
280
+ });
281
+ });