@amplytools/react-native-amply-sdk 0.4.0 → 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.
- package/AmplyReactNative.podspec +1 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +73 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +36 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +264 -0
- 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 +18 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/DeepLinkPayload.kt +15 -0
- package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +16 -0
- package/android/src/newarch/jni/AmplyReactNative-generated.cpp +19 -0
- package/dist/src/__tests__/campaignResolved.test.d.ts +2 -0
- package/dist/src/__tests__/campaignResolved.test.d.ts.map +1 -0
- package/dist/src/__tests__/campaignResolved.test.js +196 -0
- package/dist/src/index.d.ts +81 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +120 -0
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +82 -0
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ios/Sources/AmplyReactNative/AmplyModule.mm +349 -0
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +26 -0
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +10 -0
- package/package.json +1 -1
- package/src/__tests__/campaignResolved.test.ts +276 -0
- package/src/index.ts +198 -0
- package/src/nativeSpecs/NativeAmplyModule.ts +82 -0
|
@@ -44,14 +44,69 @@ static NSString* AmplyLogLevelToString(AmplyLogLevel level) {
|
|
|
44
44
|
return @"none";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Maps a JS-reported result string to the KMP CampaignResult enum. Strict semantics:
|
|
48
|
+
// anything other than a deliberate user "Dismissed" or explicit "Completed" fails open
|
|
49
|
+
// to Unavailable (proceed) — never strand the continuation on a malformed value.
|
|
50
|
+
static ASDKCampaignResult* AmplyCampaignResultFromString(NSString *result) {
|
|
51
|
+
if ([result isEqualToString:@"Completed"]) return ASDKCampaignResult.completed;
|
|
52
|
+
if ([result isEqualToString:@"Dismissed"]) return ASDKCampaignResult.dismissed;
|
|
53
|
+
if ([result isEqualToString:@"Unavailable"]) return ASDKCampaignResult.unavailable;
|
|
54
|
+
RCTLogWarn(@"[AmplyReactNative] Unknown campaign result '%@'; treating as Unavailable (fail-open)", result);
|
|
55
|
+
return ASDKCampaignResult.unavailable;
|
|
56
|
+
}
|
|
57
|
+
|
|
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.
|
|
47
65
|
@interface Amply : NativeAmplyModuleSpecBase <NativeAmplyModuleSpec, ASDKDeepLinkListener, ASDKSystemEventsListener, ASDKLogListener>
|
|
48
66
|
@property (nonatomic, strong) ASDKAmply *amplyInstance;
|
|
49
67
|
@property (nonatomic, assign) BOOL deepLinkListenerRegistered;
|
|
68
|
+
@property (nonatomic, assign) BOOL gateRegistered;
|
|
50
69
|
@property (nonatomic, assign) BOOL systemEventsListenerRegistered;
|
|
51
70
|
@property (nonatomic, assign) BOOL lifecycleObserversRegistered;
|
|
52
71
|
@property (nonatomic, assign) BOOL logListenerRegistered;
|
|
53
72
|
@property (nonatomic, assign) AmplyLogLevel currentLogLevel;
|
|
54
73
|
@property (nonatomic, strong) NSMutableArray<void (^)(ASDKAmply *)> *pendingPropertyOps;
|
|
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).
|
|
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;
|
|
55
110
|
@end
|
|
56
111
|
|
|
57
112
|
@implementation Amply
|
|
@@ -209,6 +264,204 @@ RCT_EXPORT_MODULE()
|
|
|
209
264
|
}
|
|
210
265
|
}
|
|
211
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
|
+
|
|
290
|
+
/**
|
|
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" }.
|
|
296
|
+
*/
|
|
297
|
+
- (void)trackGated:(NSString *)event
|
|
298
|
+
properties:(NSDictionary *)properties
|
|
299
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
300
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
301
|
+
{
|
|
302
|
+
if (!event || event.length == 0) {
|
|
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"}); }
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
NSDictionary *props = [properties isKindOfClass:[NSDictionary class]] ? properties : @{};
|
|
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.
|
|
316
|
+
__block BOOL settled = NO;
|
|
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
|
+
});
|
|
325
|
+
};
|
|
326
|
+
NSDictionary *failOpen = @{@"outcome": @"proceed", @"reason": @"failOpen"};
|
|
327
|
+
|
|
328
|
+
if (!self.amplyInstance) {
|
|
329
|
+
// Fail open: not initialized -> proceed immediately (KMP gate state-machine row 1).
|
|
330
|
+
RCTLogWarn(@"[AmplyReactNative] trackGated before init; failing open");
|
|
331
|
+
settle(failOpen);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@try {
|
|
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);
|
|
353
|
+
} @catch (NSException *exception) {
|
|
354
|
+
RCTLogError(@"[AmplyReactNative] trackGated threw; failing open: %@", exception.reason);
|
|
355
|
+
settle(failOpen);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
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
|
|
368
|
+
{
|
|
369
|
+
if (!mediationId) { return nil; }
|
|
370
|
+
@synchronized (self) {
|
|
371
|
+
if (!self.pendingCompletions) { return nil; }
|
|
372
|
+
id<ASDKCampaignResolution> token = self.pendingCompletions[mediationId];
|
|
373
|
+
if (token) {
|
|
374
|
+
[self.pendingCompletions removeObjectForKey:mediationId];
|
|
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;
|
|
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];
|
|
403
|
+
if (!completion) {
|
|
404
|
+
// Late/duplicate/unknown reply — the SDK token is the source of truth and is
|
|
405
|
+
// already settled (or never existed). Ignore.
|
|
406
|
+
RCTLogInfo(@"[AmplyReactNative] resolveCampaign for unknown/settled mediationId=%@; ignored", mediationId);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
ASDKCampaignResult *mapped = AmplyCampaignResultFromString(result);
|
|
410
|
+
RCTLogInfo(@"[AmplyReactNative] resolveCampaign mediationId=%@ result=%@", mediationId, result);
|
|
411
|
+
// Confirmed against the published header: CampaignResolution.resolve(result:) projects to
|
|
412
|
+
// -resolveResult:.
|
|
413
|
+
[completion resolveResult:mapped];
|
|
414
|
+
}
|
|
415
|
+
|
|
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
|
|
435
|
+
{
|
|
436
|
+
if (!self.amplyInstance) {
|
|
437
|
+
RCTLogWarn(@"[AmplyReactNative] Cannot register gate - Amply not initialized");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (!self.pendingCompletions) {
|
|
441
|
+
self.pendingCompletions = [NSMutableDictionary new];
|
|
442
|
+
}
|
|
443
|
+
if (!self.gatePresenters) {
|
|
444
|
+
self.gatePresenters = [NSMutableArray new];
|
|
445
|
+
}
|
|
446
|
+
|
|
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;
|
|
463
|
+
}
|
|
464
|
+
|
|
212
465
|
- (void)getRecentEvents:(double)limit
|
|
213
466
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
214
467
|
reject:(RCTPromiseRejectBlock)reject
|
|
@@ -675,6 +928,22 @@ static ASDKDataSetType *AmplyDataSetTypeFromDictionary(NSDictionary *type, NSStr
|
|
|
675
928
|
- (void)dealloc
|
|
676
929
|
{
|
|
677
930
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
931
|
+
|
|
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.
|
|
935
|
+
NSArray<id<ASDKCampaignResolution>> *inFlight = nil;
|
|
936
|
+
@synchronized (self) {
|
|
937
|
+
inFlight = self.pendingCompletions.allValues;
|
|
938
|
+
[self.pendingCompletions removeAllObjects];
|
|
939
|
+
}
|
|
940
|
+
for (id<ASDKCampaignResolution> completion in inFlight) {
|
|
941
|
+
@try {
|
|
942
|
+
[completion resolveResult:ASDKCampaignResult.unavailable];
|
|
943
|
+
} @catch (NSException *exception) {
|
|
944
|
+
// Best-effort cleanup during teardown.
|
|
945
|
+
}
|
|
946
|
+
}
|
|
678
947
|
}
|
|
679
948
|
|
|
680
949
|
#pragma mark - ASDKSystemEventsListener
|
|
@@ -732,3 +1001,83 @@ static ASDKDataSetType *AmplyDataSetTypeFromDictionary(NSDictionary *type, NSStr
|
|
|
732
1001
|
}
|
|
733
1002
|
|
|
734
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
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
{
|
|
24
24
|
_eventEmitterCallback("onDeepLink", value);
|
|
25
25
|
}
|
|
26
|
+
- (void)emitOnCampaignPresent:(NSDictionary *)value
|
|
27
|
+
{
|
|
28
|
+
_eventEmitterCallback("onCampaignPresent", value);
|
|
29
|
+
}
|
|
26
30
|
|
|
27
31
|
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper
|
|
28
32
|
{
|
|
@@ -56,6 +60,18 @@ namespace facebook::react {
|
|
|
56
60
|
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "track", @selector(track:resolve:reject:), args, count);
|
|
57
61
|
}
|
|
58
62
|
|
|
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
|
+
}
|
|
66
|
+
|
|
67
|
+
static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_resolveCampaign(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
|
68
|
+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "resolveCampaign", @selector(resolveCampaign:result:), args, count);
|
|
69
|
+
}
|
|
70
|
+
|
|
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
|
+
}
|
|
74
|
+
|
|
59
75
|
static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_getRecentEvents(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
|
60
76
|
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "getRecentEvents", @selector(getRecentEvents:resolve:reject:), args, count);
|
|
61
77
|
}
|
|
@@ -116,6 +132,15 @@ namespace facebook::react {
|
|
|
116
132
|
methodMap_["track"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_track};
|
|
117
133
|
setMethodArgConversionSelector(@"track", 0, @"JS_NativeAmplyModule_TrackEventPayload:");
|
|
118
134
|
|
|
135
|
+
methodMap_["trackGated"] = MethodMetadata {2, __hostFunction_NativeAmplyModuleSpecJSI_trackGated};
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
methodMap_["resolveCampaign"] = MethodMetadata {2, __hostFunction_NativeAmplyModuleSpecJSI_resolveCampaign};
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
methodMap_["registerGate"] = MethodMetadata {3, __hostFunction_NativeAmplyModuleSpecJSI_registerGate};
|
|
142
|
+
|
|
143
|
+
|
|
119
144
|
methodMap_["getRecentEvents"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_getRecentEvents};
|
|
120
145
|
|
|
121
146
|
|
|
@@ -153,6 +178,7 @@ namespace facebook::react {
|
|
|
153
178
|
|
|
154
179
|
eventEmitterMap_["onSystemEvent"] = std::make_shared<AsyncEventEmitter<id>>();
|
|
155
180
|
eventEmitterMap_["onDeepLink"] = std::make_shared<AsyncEventEmitter<id>>();
|
|
181
|
+
eventEmitterMap_["onCampaignPresent"] = std::make_shared<AsyncEventEmitter<id>>();
|
|
156
182
|
setEventEmitterCallback([&](const std::string &name, id value) {
|
|
157
183
|
static_cast<AsyncEventEmitter<id> &>(*eventEmitterMap_[name]).emit(value);
|
|
158
184
|
});
|
|
@@ -78,6 +78,15 @@ namespace JS {
|
|
|
78
78
|
- (void)track:(JS::NativeAmplyModule::TrackEventPayload &)payload
|
|
79
79
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
80
80
|
reject:(RCTPromiseRejectBlock)reject;
|
|
81
|
+
- (void)trackGated:(NSString *)event
|
|
82
|
+
properties:(NSDictionary *)properties
|
|
83
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
84
|
+
reject:(RCTPromiseRejectBlock)reject;
|
|
85
|
+
- (void)resolveCampaign:(NSString *)mediationId
|
|
86
|
+
result:(NSString *)result;
|
|
87
|
+
- (void)registerGate:(NSString *)baseUrl
|
|
88
|
+
onAbort:(NSString *)onAbort
|
|
89
|
+
timeoutMs:(double)timeoutMs;
|
|
81
90
|
- (void)getRecentEvents:(double)limit
|
|
82
91
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
83
92
|
reject:(RCTPromiseRejectBlock)reject;
|
|
@@ -107,6 +116,7 @@ facebook::react::EventEmitterCallback _eventEmitterCallback;
|
|
|
107
116
|
|
|
108
117
|
- (void)emitOnSystemEvent:(NSDictionary *)value;
|
|
109
118
|
- (void)emitOnDeepLink:(NSDictionary *)value;
|
|
119
|
+
- (void)emitOnCampaignPresent:(NSDictionary *)value;
|
|
110
120
|
@end
|
|
111
121
|
|
|
112
122
|
namespace facebook::react {
|
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.0",
|
|
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": {
|