@amplytools/react-native-amply-sdk 0.4.1 → 0.5.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 +38 -27
- package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +13 -12
- package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +195 -65
- package/android/src/main/java/tools/amply/sdk/reactnative/core/GateDecision.kt +23 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/CampaignPresentPayload.kt +4 -2
- package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +2 -2
- package/android/src/newarch/jni/AmplyReactNative-generated.cpp +6 -6
- package/dist/src/__tests__/campaignResolved.test.js +115 -56
- package/dist/src/index.d.ts +61 -37
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +79 -43
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +50 -24
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ios/Sources/AmplyReactNative/AmplyModule.mm +277 -83
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +6 -6
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +7 -5
- package/package.json +1 -1
- package/src/__tests__/campaignResolved.test.ts +169 -73
- package/src/index.ts +133 -92
- package/src/nativeSpecs/NativeAmplyModule.ts +48 -24
|
@@ -55,18 +55,58 @@ static ASDKCampaignResult* AmplyCampaignResultFromString(NSString *result) {
|
|
|
55
55
|
return ASDKCampaignResult.unavailable;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
// Gate-side protocol names confirmed against the published KMP 0.5.0-SNAPSHOT Obj-C header
|
|
59
|
+
// (AmplySDK.xcframework): ASDKCampaignPresenter (present(params:info:resolution:) + dismiss())
|
|
60
|
+
// and ASDKCampaignResolution (resolve(result:)).
|
|
61
|
+
@class AmplyGatePresenter;
|
|
62
|
+
|
|
63
|
+
// The module owns the GLOBAL routing table and the JS emitter; each registered gate gets its
|
|
64
|
+
// OWN AmplyGatePresenter instance (below) that owns the per-gate "current presentation" slot.
|
|
65
|
+
@interface Amply : NativeAmplyModuleSpecBase <NativeAmplyModuleSpec, ASDKDeepLinkListener, ASDKSystemEventsListener, ASDKLogListener>
|
|
59
66
|
@property (nonatomic, strong) ASDKAmply *amplyInstance;
|
|
60
67
|
@property (nonatomic, assign) BOOL deepLinkListenerRegistered;
|
|
61
|
-
@property (nonatomic, assign) BOOL
|
|
68
|
+
@property (nonatomic, assign) BOOL gateRegistered;
|
|
62
69
|
@property (nonatomic, assign) BOOL systemEventsListenerRegistered;
|
|
63
70
|
@property (nonatomic, assign) BOOL lifecycleObserversRegistered;
|
|
64
71
|
@property (nonatomic, assign) BOOL logListenerRegistered;
|
|
65
72
|
@property (nonatomic, assign) AmplyLogLevel currentLogLevel;
|
|
66
73
|
@property (nonatomic, strong) NSMutableArray<void (^)(ASDKAmply *)> *pendingPropertyOps;
|
|
67
|
-
//
|
|
68
|
-
//
|
|
74
|
+
// Global routing table: mediationId -> the KMP CampaignResolution token awaiting a JS reply.
|
|
75
|
+
// Shared across every registered gate because resolveCampaign(mediationId) routes by exact id —
|
|
76
|
+
// that lookup is correct regardless of which gate minted the id. Guarded by @synchronized(self).
|
|
69
77
|
@property (nonatomic, strong) NSMutableDictionary<NSString *, id<ASDKCampaignResolution>> *pendingCompletions;
|
|
78
|
+
// Retains the per-gate presenter instances; the KMP gate registry does not keep them alive.
|
|
79
|
+
@property (nonatomic, strong) NSMutableArray<AmplyGatePresenter *> *gatePresenters;
|
|
80
|
+
|
|
81
|
+
// Internal hooks used by AmplyGatePresenter (one instance per registerGate).
|
|
82
|
+
- (id<ASDKCampaignResolution>)takePendingCompletionForMediationId:(NSString *)mediationId;
|
|
83
|
+
- (void)parkPendingCompletion:(id<ASDKCampaignResolution>)resolution forMediationId:(NSString *)mediationId;
|
|
84
|
+
- (void)emitGatePresent:(NSDictionary *)payload;
|
|
85
|
+
@end
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One ASDKCampaignPresenter per registerGate registration (per url-pattern).
|
|
89
|
+
*
|
|
90
|
+
* Concurrency model: KMP presents at most one gate per url-pattern at a time (the gate is modal —
|
|
91
|
+
* you cannot show two rewarded ads, or two presentations of the same pattern, at once). KMP's
|
|
92
|
+
* -dismiss is parameterless and is dispatched to the SPECIFIC presenter instance registered for
|
|
93
|
+
* that pattern. So the "which presentation does my dismiss() target" state must live PER-PRESENTER,
|
|
94
|
+
* here in `activeMediationId` — NOT in one global slot on the module. With one global slot, an
|
|
95
|
+
* overlap of two different-pattern gates (A present, B present, A dismiss) routes A's dismiss to
|
|
96
|
+
* B's token. Per-instance state makes A's dismiss abandon only A's own live presentation.
|
|
97
|
+
*
|
|
98
|
+
* Concurrent presentations of the SAME url-pattern are not supported: a second -present overwrites
|
|
99
|
+
* `activeMediationId`, so a later -dismiss targets the latest. That is acceptable and intended
|
|
100
|
+
* given the modal contract above. The module's pendingCompletions stays global (lookup by exact id
|
|
101
|
+
* is always correct), so a JS resolveCampaign for the overwritten id still settles its own token.
|
|
102
|
+
*/
|
|
103
|
+
@interface AmplyGatePresenter : NSObject <ASDKCampaignPresenter>
|
|
104
|
+
@property (nonatomic, weak) Amply *module;
|
|
105
|
+
@property (nonatomic, copy) NSString *baseUrl;
|
|
106
|
+
// The mediationId this presenter most recently -present'ed and has not yet resolved/abandoned.
|
|
107
|
+
// -dismiss targets exactly this id, so it can only ever release THIS gate's live presentation.
|
|
108
|
+
// Guarded by @synchronized(self); only ever holds a live id (cleared when its token is taken).
|
|
109
|
+
@property (nonatomic, copy) NSString *activeMediationId;
|
|
70
110
|
@end
|
|
71
111
|
|
|
72
112
|
@implementation Amply
|
|
@@ -224,69 +264,142 @@ RCT_EXPORT_MODULE()
|
|
|
224
264
|
}
|
|
225
265
|
}
|
|
226
266
|
|
|
267
|
+
// Maps the KMP GateDecision enum/sealed value to the JS-facing decision map:
|
|
268
|
+
// { outcome: "proceed", reason: "completed" | "failOpen" } | { outcome: "cancelled" }
|
|
269
|
+
// The raw CampaignResult is never surfaced — the SDK collapses it into GateDecision.
|
|
270
|
+
static NSDictionary *AmplyGateDecisionToMap(ASDKGateDecision *decision) {
|
|
271
|
+
// Proceed carries a `reason` (Completed / FailOpen); Cancelled carries nothing.
|
|
272
|
+
if ([decision isKindOfClass:[ASDKGateDecisionProceed class]]) {
|
|
273
|
+
ASDKGateDecisionProceed *proceed = (ASDKGateDecisionProceed *)decision;
|
|
274
|
+
NSString *reason = @"failOpen";
|
|
275
|
+
if (proceed.reason == ASDKProceedReason.completed) {
|
|
276
|
+
reason = @"completed";
|
|
277
|
+
}
|
|
278
|
+
return @{@"outcome": @"proceed", @"reason": reason};
|
|
279
|
+
}
|
|
280
|
+
// Cancelled — or any unexpected shape — collapses to its terminal outcome. A
|
|
281
|
+
// Cancelled value is the only non-proceed terminal in the frozen API.
|
|
282
|
+
if ([decision isKindOfClass:[ASDKGateDecisionCancelled class]]) {
|
|
283
|
+
return @{@"outcome": @"cancelled"};
|
|
284
|
+
}
|
|
285
|
+
// Defensive: an unknown decision subtype fails open rather than stranding the caller.
|
|
286
|
+
RCTLogWarn(@"[AmplyReactNative] Unknown GateDecision subtype; failing open");
|
|
287
|
+
return @{@"outcome": @"proceed", @"reason": @"failOpen"};
|
|
288
|
+
}
|
|
289
|
+
|
|
227
290
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* (defensive) double-settle.
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* The raw CampaignResult is never surfaced here — the SDK collapses it into the
|
|
235
|
-
* onComplete/onCancel decision before this bridge ever sees it.
|
|
291
|
+
* Gated form of track. Backed by the K/N-generated async selector for the KMP
|
|
292
|
+
* `suspend fun trackGated(event:props:): GateDecision`. The resolve block is settled
|
|
293
|
+
* exactly once with the collapsed decision map; __block + a settled flag guards the
|
|
294
|
+
* (defensive) double-settle. This NEVER rejects: not-initialized, a thrown exception,
|
|
295
|
+
* or a K/N error all fail open to { outcome: "proceed", reason: "failOpen" }.
|
|
236
296
|
*/
|
|
237
|
-
- (void)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
297
|
+
- (void)trackGated:(NSString *)event
|
|
298
|
+
properties:(NSDictionary *)properties
|
|
299
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
300
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
241
301
|
{
|
|
242
302
|
if (!event || event.length == 0) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
303
|
+
// Even an invalid event fails open rather than rejecting — the JS contract is
|
|
304
|
+
// "trackGated never rejects".
|
|
305
|
+
RCTLogWarn(@"[AmplyReactNative] trackGated with empty event; failing open");
|
|
306
|
+
if (resolve) { resolve(@{@"outcome": @"proceed", @"reason": @"failOpen"}); }
|
|
246
307
|
return;
|
|
247
308
|
}
|
|
248
309
|
|
|
249
310
|
NSDictionary *props = [properties isKindOfClass:[NSDictionary class]] ? properties : @{};
|
|
250
311
|
|
|
312
|
+
// Contract: resolve EXACTLY ONCE, ON MAIN. Every settle path — fail-open before init, the
|
|
313
|
+
// K/N success completion, the K/N error/cancellation completion, and the @catch — funnels
|
|
314
|
+
// through here. We always hop to the main queue (which serializes the once-guard, so the
|
|
315
|
+
// flag needs no further synchronization) and resolve at most once.
|
|
251
316
|
__block BOOL settled = NO;
|
|
252
|
-
void (^settle)(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
resolve
|
|
257
|
-
|
|
317
|
+
void (^settle)(NSDictionary *) = ^(NSDictionary *decision) {
|
|
318
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
319
|
+
if (settled) { return; }
|
|
320
|
+
settled = YES;
|
|
321
|
+
if (resolve) {
|
|
322
|
+
resolve(decision);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
258
325
|
};
|
|
326
|
+
NSDictionary *failOpen = @{@"outcome": @"proceed", @"reason": @"failOpen"};
|
|
259
327
|
|
|
260
328
|
if (!self.amplyInstance) {
|
|
261
|
-
// Fail open: not initialized -> proceed immediately (KMP state-machine row 1).
|
|
262
|
-
RCTLogWarn(@"[AmplyReactNative]
|
|
263
|
-
settle(
|
|
329
|
+
// Fail open: not initialized -> proceed immediately (KMP gate state-machine row 1).
|
|
330
|
+
RCTLogWarn(@"[AmplyReactNative] trackGated before init; failing open");
|
|
331
|
+
settle(failOpen);
|
|
264
332
|
return;
|
|
265
333
|
}
|
|
266
334
|
|
|
267
335
|
@try {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
336
|
+
// Confirmed against the published KMP 0.5.0-SNAPSHOT header: the ASDKAmply facade's
|
|
337
|
+
// `suspend fun trackGated(event:properties:)` projects to
|
|
338
|
+
// `-trackGatedEvent:properties:completionHandler:` with a (ASDKGateDecision*, NSError*) handler.
|
|
339
|
+
[self.amplyInstance trackGatedEvent:event
|
|
340
|
+
properties:props
|
|
341
|
+
completionHandler:^(ASDKGateDecision *decision, NSError *error) {
|
|
342
|
+
if (error) {
|
|
343
|
+
// A coroutine error/cancellation fails open — never strand the caller. settle() routes
|
|
344
|
+
// this onto main with the same once-guard as the success path (resolve once, on main).
|
|
345
|
+
RCTLogWarn(@"[AmplyReactNative] trackGated coroutine error; failing open: %@", error.localizedDescription);
|
|
346
|
+
settle(failOpen);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// settle() already hops to main to match the deeplink/present delivery contract.
|
|
350
|
+
settle(AmplyGateDecisionToMap(decision));
|
|
351
|
+
}];
|
|
352
|
+
RCTLogInfo(@"[AmplyReactNative] trackGated dispatched: %@", event);
|
|
273
353
|
} @catch (NSException *exception) {
|
|
274
|
-
RCTLogError(@"[AmplyReactNative]
|
|
275
|
-
settle(
|
|
354
|
+
RCTLogError(@"[AmplyReactNative] trackGated threw; failing open: %@", exception.reason);
|
|
355
|
+
settle(failOpen);
|
|
276
356
|
}
|
|
277
357
|
}
|
|
278
358
|
|
|
279
|
-
|
|
280
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Atomically removes and returns the token parked under `mediationId` from the GLOBAL routing
|
|
361
|
+
* table. Returning the token to exactly one caller is what makes resolve and dismiss mutually-
|
|
362
|
+
* exclusive and at-most-once: whoever takes the token first owns the outcome; every later path
|
|
363
|
+
* gets nil and becomes a no-op. This is the single point that prevents the pending-resolution
|
|
364
|
+
* leak on timeout/cancel/dismiss. (The per-gate "current presentation" slot lives on each
|
|
365
|
+
* AmplyGatePresenter, not here — the presenter clears its own slot around -dismiss.)
|
|
366
|
+
*/
|
|
367
|
+
- (id<ASDKCampaignResolution>)takePendingCompletionForMediationId:(NSString *)mediationId
|
|
281
368
|
{
|
|
282
|
-
|
|
369
|
+
if (!mediationId) { return nil; }
|
|
283
370
|
@synchronized (self) {
|
|
284
|
-
if (!self.pendingCompletions) { return; }
|
|
285
|
-
|
|
286
|
-
if (
|
|
371
|
+
if (!self.pendingCompletions) { return nil; }
|
|
372
|
+
id<ASDKCampaignResolution> token = self.pendingCompletions[mediationId];
|
|
373
|
+
if (token) {
|
|
287
374
|
[self.pendingCompletions removeObjectForKey:mediationId];
|
|
288
375
|
}
|
|
376
|
+
return token;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Parks a resolution token in the global routing table under `mediationId`. */
|
|
381
|
+
- (void)parkPendingCompletion:(id<ASDKCampaignResolution>)resolution forMediationId:(NSString *)mediationId
|
|
382
|
+
{
|
|
383
|
+
@synchronized (self) {
|
|
384
|
+
if (!self.pendingCompletions) {
|
|
385
|
+
self.pendingCompletions = [NSMutableDictionary new];
|
|
386
|
+
}
|
|
387
|
+
self.pendingCompletions[mediationId] = resolution;
|
|
289
388
|
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Surfaces a gate present dispatch to JS. Emitter delivery is async; hop to main to match deeplink. */
|
|
392
|
+
- (void)emitGatePresent:(NSDictionary *)payload
|
|
393
|
+
{
|
|
394
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
395
|
+
[self emitOnCampaignPresent:payload];
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
- (void)resolveCampaign:(NSString *)mediationId
|
|
400
|
+
result:(NSString *)result
|
|
401
|
+
{
|
|
402
|
+
id<ASDKCampaignResolution> completion = [self takePendingCompletionForMediationId:mediationId];
|
|
290
403
|
if (!completion) {
|
|
291
404
|
// Late/duplicate/unknown reply — the SDK token is the source of truth and is
|
|
292
405
|
// already settled (or never existed). Ignore.
|
|
@@ -295,59 +408,58 @@ RCT_EXPORT_MODULE()
|
|
|
295
408
|
}
|
|
296
409
|
ASDKCampaignResult *mapped = AmplyCampaignResultFromString(result);
|
|
297
410
|
RCTLogInfo(@"[AmplyReactNative] resolveCampaign mediationId=%@ result=%@", mediationId, result);
|
|
411
|
+
// Confirmed against the published header: CampaignResolution.resolve(result:) projects to
|
|
412
|
+
// -resolveResult:.
|
|
298
413
|
[completion resolveResult:mapped];
|
|
299
414
|
}
|
|
300
415
|
|
|
301
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Register the bridge as the gate presenter for `baseUrl`. JS is the single capability
|
|
418
|
+
* registry and routes by baseUrl on its side; the native gate carries the abort policy
|
|
419
|
+
* and timeout.
|
|
420
|
+
*
|
|
421
|
+
* FLAG (native-compile gate): the frozen KMP 0.5.0 API is
|
|
422
|
+
* `registerGate(baseUrl, presenter, onAbort, timeoutMs)`.
|
|
423
|
+
* The selector below is written to the documented K/N convention
|
|
424
|
+
* (`-registerGateBaseUrl:presenter:onAbort:timeoutMs:`) and the ASDKAbortPolicy
|
|
425
|
+
* enum-case names (.cancel / .proceed) are assumed. BOTH the selector and the
|
|
426
|
+
* AbortPolicy projection MUST be confirmed against the published header before publish.
|
|
427
|
+
*
|
|
428
|
+
* Each registration gets its OWN AmplyGatePresenter instance owning its OWN "current
|
|
429
|
+
* presentation" slot, so a dismiss() delivered to this gate's presenter can only abandon THIS
|
|
430
|
+
* gate's live presentation — never one belonging to a different url-pattern's gate.
|
|
431
|
+
*/
|
|
432
|
+
- (void)registerGate:(NSString *)baseUrl
|
|
433
|
+
onAbort:(NSString *)onAbort
|
|
434
|
+
timeoutMs:(double)timeoutMs
|
|
302
435
|
{
|
|
303
436
|
if (!self.amplyInstance) {
|
|
304
|
-
RCTLogWarn(@"[AmplyReactNative] Cannot register
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (self.campaignPresenterRegistered) {
|
|
308
|
-
RCTLogInfo(@"[AmplyReactNative] Campaign presenter already registered");
|
|
437
|
+
RCTLogWarn(@"[AmplyReactNative] Cannot register gate - Amply not initialized");
|
|
309
438
|
return;
|
|
310
439
|
}
|
|
311
440
|
if (!self.pendingCompletions) {
|
|
312
441
|
self.pendingCompletions = [NSMutableDictionary new];
|
|
313
442
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
[self.amplyInstance registerCampaignPresenterPresenter:self];
|
|
317
|
-
self.campaignPresenterRegistered = YES;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
#pragma mark - ASDKCampaignPresenter
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Called by the KMP SDK when a campaign dispatches a present (blocking) action.
|
|
324
|
-
* We mint a mediationId, park the completion token, and surface the dispatch to JS.
|
|
325
|
-
* JS later replies via resolveCampaign:result:.
|
|
326
|
-
*/
|
|
327
|
-
- (void)onCampaignPresentUrl:(NSString *)url
|
|
328
|
-
info:(NSDictionary<NSString *, id> *)info
|
|
329
|
-
completion:(id<ASDKCampaignResolution>)completion
|
|
330
|
-
{
|
|
331
|
-
NSString *mediationId = [[NSUUID UUID] UUIDString];
|
|
332
|
-
@synchronized (self) {
|
|
333
|
-
if (!self.pendingCompletions) {
|
|
334
|
-
self.pendingCompletions = [NSMutableDictionary new];
|
|
335
|
-
}
|
|
336
|
-
self.pendingCompletions[mediationId] = completion;
|
|
443
|
+
if (!self.gatePresenters) {
|
|
444
|
+
self.gatePresenters = [NSMutableArray new];
|
|
337
445
|
}
|
|
338
446
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
447
|
+
ASDKAbortPolicy *policy = [onAbort isEqualToString:@"proceed"]
|
|
448
|
+
? ASDKAbortPolicy.proceed
|
|
449
|
+
: ASDKAbortPolicy.cancel;
|
|
450
|
+
|
|
451
|
+
AmplyGatePresenter *presenter = [[AmplyGatePresenter alloc] init];
|
|
452
|
+
presenter.module = self;
|
|
453
|
+
presenter.baseUrl = baseUrl;
|
|
454
|
+
// Retain the presenter — the KMP gate registry does not keep it alive.
|
|
455
|
+
[self.gatePresenters addObject:presenter];
|
|
456
|
+
|
|
457
|
+
RCTLogInfo(@"[AmplyReactNative] Registering gate baseUrl=%@ onAbort=%@ timeoutMs=%f", baseUrl, onAbort, timeoutMs);
|
|
458
|
+
[self.amplyInstance registerGateBaseUrl:baseUrl
|
|
459
|
+
presenter:presenter
|
|
460
|
+
onAbort:policy
|
|
461
|
+
timeoutMs:(int64_t)timeoutMs];
|
|
462
|
+
self.gateRegistered = YES;
|
|
351
463
|
}
|
|
352
464
|
|
|
353
465
|
- (void)getRecentEvents:(double)limit
|
|
@@ -817,7 +929,9 @@ static ASDKDataSetType *AmplyDataSetTypeFromDictionary(NSDictionary *type, NSStr
|
|
|
817
929
|
{
|
|
818
930
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
819
931
|
|
|
820
|
-
// Fail open any in-flight
|
|
932
|
+
// Fail open any in-flight gate presentations so no caller is left hanging. Each per-gate
|
|
933
|
+
// presenter clears its own current-presentation slot when its token is taken below; on dealloc
|
|
934
|
+
// the presenters themselves are released with the module.
|
|
821
935
|
NSArray<id<ASDKCampaignResolution>> *inFlight = nil;
|
|
822
936
|
@synchronized (self) {
|
|
823
937
|
inFlight = self.pendingCompletions.allValues;
|
|
@@ -887,3 +1001,83 @@ static ASDKDataSetType *AmplyDataSetTypeFromDictionary(NSDictionary *type, NSStr
|
|
|
887
1001
|
}
|
|
888
1002
|
|
|
889
1003
|
@end
|
|
1004
|
+
|
|
1005
|
+
#pragma mark - AmplyGatePresenter (one instance per registerGate / url-pattern)
|
|
1006
|
+
|
|
1007
|
+
@implementation AmplyGatePresenter
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Called by the KMP SDK when this gate dispatches its (blocking) presentation. We mint a
|
|
1011
|
+
* mediationId, park the resolution token in the module's GLOBAL routing table, record it as
|
|
1012
|
+
* THIS presenter's current presentation, and surface the dispatch to JS. JS later replies via
|
|
1013
|
+
* resolveCampaign:result: (which routes by exact id, so it is unaffected by overlap).
|
|
1014
|
+
*
|
|
1015
|
+
* Confirmed against the published header: the presenter signature is
|
|
1016
|
+
* `present(params:info:resolution:)` -> `-presentParams:info:resolution:`,
|
|
1017
|
+
* with params projected as NSDictionary<NSString *, NSString *>.
|
|
1018
|
+
*/
|
|
1019
|
+
- (void)presentParams:(NSDictionary<NSString *, NSString *> *)params
|
|
1020
|
+
info:(NSDictionary<NSString *, id> *)info
|
|
1021
|
+
resolution:(id<ASDKCampaignResolution>)resolution
|
|
1022
|
+
{
|
|
1023
|
+
Amply *module = self.module;
|
|
1024
|
+
if (!module) {
|
|
1025
|
+
// Module gone (teardown race) — drop the token so the SDK's own abort machinery proceeds.
|
|
1026
|
+
RCTLogWarn(@"[AmplyReactNative] Gate[%@] present after module teardown; dropping", self.baseUrl);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
NSString *mediationId = [[NSUUID UUID] UUIDString];
|
|
1031
|
+
[module parkPendingCompletion:resolution forMediationId:mediationId];
|
|
1032
|
+
// Record THIS gate's live presentation so -dismiss releases exactly this token on abandon. A
|
|
1033
|
+
// second present() on this same presenter overwrites it (concurrent same-pattern not supported;
|
|
1034
|
+
// latest wins) — the prior id still lives in the global table for its own JS reply.
|
|
1035
|
+
@synchronized (self) {
|
|
1036
|
+
self.activeMediationId = mediationId;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// `info` carries the action url under @"url"; surface it for JS-side baseUrl routing.
|
|
1040
|
+
NSString *url = [info[@"url"] isKindOfClass:[NSString class]] ? info[@"url"] : @"";
|
|
1041
|
+
|
|
1042
|
+
RCTLogInfo(@"[AmplyReactNative] Gate[%@] dispatching present mediationId=%@ url=%@", self.baseUrl, mediationId, url);
|
|
1043
|
+
|
|
1044
|
+
NSDictionary *payload = @{
|
|
1045
|
+
@"url": url,
|
|
1046
|
+
@"params": params ?: @{},
|
|
1047
|
+
@"info": info ?: @{},
|
|
1048
|
+
@"mediationId": mediationId
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
[module emitGatePresent:payload];
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Tear down any UI this presenter put on screen WITHOUT producing a result. Called by the SDK
|
|
1056
|
+
* when THIS gate's caller coroutine is cancelled/times out and the resolution token is abandoned.
|
|
1057
|
+
*
|
|
1058
|
+
* Presentation lives entirely in JS here (no native gate UI), so there is nothing to tear down —
|
|
1059
|
+
* BUT the parked token MUST be released or the global pendingCompletions leaks on every
|
|
1060
|
+
* timeout/cancel. We take THIS presenter's current presentation's token (at-most-once via
|
|
1061
|
+
* -takePendingCompletionForMediationId:) and simply drop it: the SDK's own abort machinery owns
|
|
1062
|
+
* the outcome here, so we must NOT also resolve it. A subsequent JS resolveCampaign: for the same
|
|
1063
|
+
* id finds nothing and is a harmless no-op. Crucially, because the active id is per-presenter, an
|
|
1064
|
+
* overlapping different-pattern gate's presentation can never be the one we abandon here.
|
|
1065
|
+
*/
|
|
1066
|
+
- (void)dismiss
|
|
1067
|
+
{
|
|
1068
|
+
NSString *activeId = nil;
|
|
1069
|
+
@synchronized (self) {
|
|
1070
|
+
activeId = self.activeMediationId;
|
|
1071
|
+
self.activeMediationId = nil;
|
|
1072
|
+
}
|
|
1073
|
+
if (!activeId) {
|
|
1074
|
+
RCTLogInfo(@"[AmplyReactNative] Gate[%@] dismiss() with no active presentation", self.baseUrl);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
Amply *module = self.module;
|
|
1078
|
+
id<ASDKCampaignResolution> abandoned = [module takePendingCompletionForMediationId:activeId];
|
|
1079
|
+
RCTLogInfo(@"[AmplyReactNative] Gate[%@] dismiss() releasing mediationId=%@ (token %@)",
|
|
1080
|
+
self.baseUrl, activeId, abandoned ? @"dropped" : @"already settled");
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
@end
|
|
@@ -60,16 +60,16 @@ namespace facebook::react {
|
|
|
60
60
|
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "track", @selector(track:resolve:reject:), args, count);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
static facebook::jsi::Value
|
|
64
|
-
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "
|
|
63
|
+
static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_trackGated(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
|
64
|
+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "trackGated", @selector(trackGated:properties:resolve:reject:), args, count);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_resolveCampaign(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
|
68
68
|
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "resolveCampaign", @selector(resolveCampaign:result:), args, count);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
static facebook::jsi::Value
|
|
72
|
-
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "
|
|
71
|
+
static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_registerGate(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
|
72
|
+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "registerGate", @selector(registerGate:onAbort:timeoutMs:), args, count);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_getRecentEvents(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
|
@@ -132,13 +132,13 @@ namespace facebook::react {
|
|
|
132
132
|
methodMap_["track"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_track};
|
|
133
133
|
setMethodArgConversionSelector(@"track", 0, @"JS_NativeAmplyModule_TrackEventPayload:");
|
|
134
134
|
|
|
135
|
-
methodMap_["
|
|
135
|
+
methodMap_["trackGated"] = MethodMetadata {2, __hostFunction_NativeAmplyModuleSpecJSI_trackGated};
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
methodMap_["resolveCampaign"] = MethodMetadata {2, __hostFunction_NativeAmplyModuleSpecJSI_resolveCampaign};
|
|
139
139
|
|
|
140
140
|
|
|
141
|
-
methodMap_["
|
|
141
|
+
methodMap_["registerGate"] = MethodMetadata {3, __hostFunction_NativeAmplyModuleSpecJSI_registerGate};
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
methodMap_["getRecentEvents"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_getRecentEvents};
|
|
@@ -78,13 +78,15 @@ namespace JS {
|
|
|
78
78
|
- (void)track:(JS::NativeAmplyModule::TrackEventPayload &)payload
|
|
79
79
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
80
80
|
reject:(RCTPromiseRejectBlock)reject;
|
|
81
|
-
- (void)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
- (void)trackGated:(NSString *)event
|
|
82
|
+
properties:(NSDictionary *)properties
|
|
83
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
84
|
+
reject:(RCTPromiseRejectBlock)reject;
|
|
85
85
|
- (void)resolveCampaign:(NSString *)mediationId
|
|
86
86
|
result:(NSString *)result;
|
|
87
|
-
- (void)
|
|
87
|
+
- (void)registerGate:(NSString *)baseUrl
|
|
88
|
+
onAbort:(NSString *)onAbort
|
|
89
|
+
timeoutMs:(double)timeoutMs;
|
|
88
90
|
- (void)getRecentEvents:(double)limit
|
|
89
91
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
90
92
|
reject:(RCTPromiseRejectBlock)reject;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amplytools/react-native-amply-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|