@purchasely/cordova-plugin-purchasely 5.7.3 → 6.0.0-rc.3

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 (33) hide show
  1. package/README.md +12 -8
  2. package/__tests__/Purchasely.test.js +307 -92
  3. package/example/config.xml +1 -11
  4. package/example/e2e/README.md +44 -0
  5. package/example/e2e/helpers/driver.js +70 -0
  6. package/example/e2e/package.json +19 -0
  7. package/example/e2e/specs/bridge.e2e.js +51 -0
  8. package/example/e2e/specs/dismiss.e2e.js +37 -0
  9. package/example/e2e/tools/ci_run_e2e.sh +49 -0
  10. package/example/e2e/tools/ci_run_e2e_ios.sh +46 -0
  11. package/example/e2e/wdio.android.conf.js +21 -0
  12. package/example/e2e/wdio.ios.conf.js +19 -0
  13. package/example/e2e/wdio.shared.conf.js +28 -0
  14. package/example/package-lock.json +59 -205
  15. package/example/package.json +7 -6
  16. package/example/www/index.html +1 -7
  17. package/example/www/js/index.js +113 -92
  18. package/package.json +1 -1
  19. package/plugin.xml +4 -22
  20. package/src/android/PurchaselyPlugin.kt +529 -440
  21. package/src/ios/CDVPurchasely+Events.h +1 -1
  22. package/src/ios/CDVPurchasely+UserAttributes.h +1 -1
  23. package/src/ios/CDVPurchasely+UserAttributes.m +2 -0
  24. package/src/ios/CDVPurchasely.h +21 -16
  25. package/src/ios/CDVPurchasely.m +497 -261
  26. package/www/Purchasely.js +208 -40
  27. package/src/android/PLYProductActivity.kt +0 -168
  28. package/src/android/PLYSubscriptionsActivity.java +0 -37
  29. package/src/android/activity_ply_product_activity.xml +0 -6
  30. package/src/android/activity_ply_subscriptions_activity.xml +0 -6
  31. package/src/android/theme_purchasely_fullscreen.xml +0 -6
  32. package/src/android/theme_purchasely_fullscreen_v23.xml +0 -6
  33. package/src/android/theme_purchasely_fullscreen_v23_light_status_bar.xml +0 -7
@@ -8,6 +8,7 @@
8
8
  #import "CDVPurchasely.h"
9
9
  #import "Purchasely_Hybrid.h"
10
10
  #import "CDVPurchasely+Events.h"
11
+ #import "CDVPurchasely+UserAttributes.h"
11
12
  #import "UIColor+PLYHelper.h"
12
13
 
13
14
  @implementation CDVPurchasely
@@ -16,35 +17,88 @@
16
17
  self = [super init];
17
18
 
18
19
  self.presentationsLoaded = [NSMutableArray new];
19
- self.shouldReopenPaywall = NO;
20
+ self.actionInterceptorCallbackIds = [NSMutableDictionary new];
21
+ self.pendingInterceptCompletions = [NSMutableDictionary new];
20
22
 
21
23
  return self;
22
24
  }
23
25
 
24
26
  - (void)start:(CDVInvokedUrlCommand*)command {
25
- NSString *apiKey = [command argumentAtIndex:0];
26
- BOOL storeKit1 = [[command argumentAtIndex:2] boolValue];
27
- NSString *userId = [command argumentAtIndex:3];
28
- NSInteger logLevel = [[command argumentAtIndex:4] intValue];
29
- NSInteger runningMode = [[command argumentAtIndex:5] intValue];
30
- NSString *purchaselySdkVersion = [command argumentAtIndex:6];
27
+ // v6: a single options dictionary (see the JS↔native contract), no longer positional args.
28
+ NSDictionary *opts = [command argumentAtIndex:0];
29
+ if (![opts isKindOfClass:[NSDictionary class]]) {
30
+ [self failureFor:command resultString:@"start requires an options object"];
31
+ return;
32
+ }
33
+
34
+ NSString *apiKey = opts[@"apiKey"];
35
+ if (![apiKey isKindOfClass:[NSString class]] || apiKey.length == 0) {
36
+ [self failureFor:command resultString:@"apiKey is required"];
37
+ return;
38
+ }
31
39
 
32
- [Purchasely setSdkBridgeVersion:purchaselySdkVersion];
40
+ PurchaselyBuilder *builder = [Purchasely apiKey:apiKey];
41
+ builder = [builder appTechnology:PLYAppTechnologyCordova];
33
42
 
34
- [Purchasely setAppTechnology:PLYAppTechnologyCordova];
43
+ NSString *sdkVersion = opts[@"sdkVersion"];
44
+ if ([sdkVersion isKindOfClass:[NSString class]]) {
45
+ builder = [builder sdkBridgeVersion:sdkVersion];
46
+ }
35
47
 
36
- [Purchasely startWithAPIKey:apiKey
37
- appUserId:userId
38
- runningMode:runningMode
39
- paywallActionsInterceptor:nil
40
- storekitSettings: storeKit1 ? [StorekitSettings storeKit1] : [StorekitSettings storeKit2]
41
- logLevel:logLevel
42
- initialized:^(BOOL initialized, NSError * _Nullable error) {
48
+ NSString *appUserId = opts[@"appUserId"];
49
+ if ([appUserId isKindOfClass:[NSString class]] && appUserId.length > 0) {
50
+ builder = [builder appUserId:appUserId];
51
+ }
52
+
53
+ // runningMode is a string ("observer"/"full"), mapped by NAME (iOS enum: observer=2, full=3).
54
+ NSString *runningMode = opts[@"runningMode"];
55
+ enum PLYRunningMode mode = PLYRunningModeObserver;
56
+ if ([runningMode isKindOfClass:[NSString class]] && [runningMode.lowercaseString isEqualToString:@"full"]) {
57
+ mode = PLYRunningModeFull;
58
+ }
59
+ builder = [builder runningMode:mode];
60
+
61
+ NSNumber *logLevel = opts[@"logLevel"];
62
+ if ([logLevel isKindOfClass:[NSNumber class]]) {
63
+ builder = [builder logLevel:(enum PLYLogLevel)logLevel.integerValue];
64
+ }
65
+
66
+ // StoreKit selection (iOS): storeKit1 bool OR storekitVersion == "storeKit1" forces StoreKit 1.
67
+ BOOL storeKit1 = NO;
68
+ NSNumber *storeKit1Num = opts[@"storeKit1"];
69
+ if ([storeKit1Num isKindOfClass:[NSNumber class]]) {
70
+ storeKit1 = storeKit1Num.boolValue;
71
+ }
72
+ NSString *storekitVersion = opts[@"storekitVersion"];
73
+ if ([storekitVersion isKindOfClass:[NSString class]] && [storekitVersion isEqualToString:@"storeKit1"]) {
74
+ storeKit1 = YES;
75
+ }
76
+ builder = [builder storekitSettings: storeKit1 ? [StorekitSettings storeKit1] : [StorekitSettings storeKit2]];
77
+
78
+ NSNumber *allowDeeplink = opts[@"allowDeeplink"];
79
+ if ([allowDeeplink isKindOfClass:[NSNumber class]]) {
80
+ builder = [builder allowDeeplink:allowDeeplink.boolValue];
81
+ }
82
+
83
+ NSNumber *allowCampaigns = opts[@"allowCampaigns"];
84
+ if ([allowCampaigns isKindOfClass:[NSNumber class]]) {
85
+ builder = [builder allowCampaigns:allowCampaigns.boolValue];
86
+ }
87
+
88
+ // Cold-start deeplink URL captured at launch (handled automatically once start completes).
89
+ NSString *deeplink = opts[@"deeplink"];
90
+ if ([deeplink isKindOfClass:[NSString class]] && deeplink.length > 0) {
91
+ NSURL *url = [NSURL URLWithString:deeplink];
92
+ if (url != nil) {
93
+ builder = [builder handleDeeplink:url];
94
+ }
95
+ }
43
96
 
97
+ [builder startWithInitialized:^(NSError * _Nullable error) {
44
98
  if (error != nil) {
45
99
  [self failureFor:command resultString: error.localizedDescription];
46
100
  } else {
47
- [self successFor:command resultBool:initialized];
101
+ [self successFor:command resultBool:YES];
48
102
  }
49
103
  }];
50
104
  }
@@ -80,7 +134,8 @@
80
134
  }
81
135
 
82
136
  NSInteger rawAttribute = [attributeNumber integerValue];
83
- PLYAttribute *attribute = nil;
137
+ PLYAttribute attribute = PLYAttributeFirebaseAppInstanceId;
138
+ BOOL attributeFound = YES;
84
139
 
85
140
  switch (rawAttribute) {
86
141
  case CordovaPLYAttributeFirebaseAppInstanceId:
@@ -146,10 +201,13 @@
146
201
  case CordovaPLYAttributeBatchCustomUserId:
147
202
  attribute = PLYAttributeBatchCustomUserId;
148
203
  break;
204
+ default:
205
+ attributeFound = NO;
206
+ break;
149
207
  }
150
208
 
151
209
 
152
- if (attribute == nil) {
210
+ if (!attributeFound) {
153
211
  return;
154
212
  }
155
213
 
@@ -161,14 +219,19 @@
161
219
  [self successFor:command resultString:anonymousId];
162
220
  }
163
221
 
164
- - (void)readyToOpenDeeplink:(CDVInvokedUrlCommand*)command {
165
- BOOL isReadyToPurchase = [[command argumentAtIndex:0] boolValue];
166
- [Purchasely readyToOpenDeeplink: isReadyToPurchase];
222
+ - (void)allowDeeplink:(CDVInvokedUrlCommand*)command {
223
+ BOOL allow = [[command argumentAtIndex:0] boolValue];
224
+ [Purchasely allowDeeplink: allow];
167
225
  }
168
226
 
169
- - (void)setDefaultPresentationResultHandler:(CDVInvokedUrlCommand*)command {
170
- [Purchasely setDefaultPresentationResultHandler:^(enum PLYProductViewControllerResult result, PLYPlan * _Nullable plan) {
171
- NSDictionary *resultDict = [self resultDictionaryForPresentationController:result plan:plan];
227
+ - (void)allowCampaigns:(CDVInvokedUrlCommand*)command {
228
+ BOOL allow = [[command argumentAtIndex:0] boolValue];
229
+ [Purchasely allowCampaigns: allow];
230
+ }
231
+
232
+ - (void)setDefaultPresentationDismissHandler:(CDVInvokedUrlCommand*)command {
233
+ [Purchasely setDefaultPresentationDismissHandler:^(PLYPresentationOutcome * _Nonnull outcome) {
234
+ NSDictionary *resultDict = [self resultDictionaryForOutcome:outcome];
172
235
 
173
236
  CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:resultDict];
174
237
  [pluginResult setKeepCallbackAsBool:YES];
@@ -176,118 +239,97 @@
176
239
  }];
177
240
  }
178
241
 
179
- - (void)presentPresentationWithIdentifier:(CDVInvokedUrlCommand*)command {
180
- NSString *presentationVendorId = [command argumentAtIndex:0];
181
- NSString *contentId = [command argumentAtIndex:1];
182
- BOOL isFullscreen = [[command argumentAtIndex:2] boolValue];
183
-
184
- UIViewController *ctrl = [Purchasely presentationControllerWith:presentationVendorId contentId:contentId loaded:nil completion:^(enum PLYProductViewControllerResult result, PLYPlan * _Nullable plan) {
185
- NSDictionary *resultDict = [self resultDictionaryForPresentationController:result plan:plan];
186
- [self successFor:command resultDict:resultDict];
187
- }];
188
-
189
- if (ctrl != nil) {
190
- UINavigationController *navCtrl = [[UINavigationController alloc] initWithRootViewController:ctrl];
191
- [navCtrl.navigationBar setTranslucent:YES];
192
- [navCtrl.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
193
- [navCtrl.navigationBar setShadowImage: [UIImage new]];
194
- [navCtrl.navigationBar setTintColor: [UIColor whiteColor]];
195
-
196
- self.presentedPresentationViewController = navCtrl;
197
-
198
- if (isFullscreen) {
199
- navCtrl.modalPresentationStyle = UIModalPresentationFullScreen;
200
- }
201
- [Purchasely showController:navCtrl type: PLYUIControllerTypeProductPage from:nil];
202
- }
242
+ // v6: stop receiving default (campaign/deeplink) presentation dismiss outcomes.
243
+ - (void)removeDefaultPresentationDismissHandler:(CDVInvokedUrlCommand*)command {
244
+ [Purchasely setDefaultPresentationDismissHandler:nil];
203
245
  }
204
246
 
205
- - (void)presentPresentationForPlacement:(CDVInvokedUrlCommand*)command {
206
- NSString *placementVendorId = [command argumentAtIndex:0];
207
- NSString *contentId = [command argumentAtIndex:1];
208
- BOOL isFullscreen = [[command argumentAtIndex:2] boolValue];
209
-
210
- UIViewController *ctrl = [Purchasely presentationControllerFor:placementVendorId contentId:contentId loaded:nil completion:^(enum PLYProductViewControllerResult result, PLYPlan * _Nullable plan) {
211
- NSDictionary *resultDict = [self resultDictionaryForPresentationController:result plan:plan];
212
- [self successFor:command resultDict:resultDict];
213
- }];
214
-
215
- if (ctrl != nil) {
216
- UINavigationController *navCtrl = [[UINavigationController alloc] initWithRootViewController:ctrl];
217
- [navCtrl.navigationBar setTranslucent:YES];
218
- [navCtrl.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
219
- [navCtrl.navigationBar setShadowImage: [UIImage new]];
220
- [navCtrl.navigationBar setTintColor: [UIColor whiteColor]];
221
-
222
- self.presentedPresentationViewController = navCtrl;
223
-
224
- if (isFullscreen) {
225
- navCtrl.modalPresentationStyle = UIModalPresentationFullScreen;
247
+ // v6: wire a builder's presentation lifecycle onto a single Cordova callback.
248
+ // onPresented / onCloseRequested stream keep-alive envelopes ({ event: ... });
249
+ // onDismissed delivers the final outcome (no `event` key) and ends the callback.
250
+ // Must run on the main queue (UIKit). `transition` is the JS transition object.
251
+ - (void)displayWithBuilder:(PLYPresentationBuilder *)builder
252
+ contentId:(NSString *)contentId
253
+ transition:(id)transition
254
+ command:(CDVInvokedUrlCommand *)command {
255
+ if ([contentId isKindOfClass:[NSString class]]) {
256
+ builder = [builder contentId:contentId];
257
+ }
258
+ if ([transition isKindOfClass:[NSDictionary class]]) {
259
+ NSString *bgHex = ((NSDictionary *)transition)[@"backgroundColor"];
260
+ if ([bgHex isKindOfClass:[NSString class]]) {
261
+ UIColor *bg = [UIColor ply_fromHex:bgHex];
262
+ if (bg != nil) { builder = [builder backgroundColor:bg]; }
226
263
  }
227
- [Purchasely showController:navCtrl type: PLYUIControllerTypeProductPage from:nil];
228
264
  }
229
- }
230
265
 
231
- - (void)presentPlanWithIdentifier:(CDVInvokedUrlCommand*)command {
232
- NSString *planVendorId = [command argumentAtIndex:0];
233
- NSString *presentationVendorId = [command argumentAtIndex:1];
234
- NSString *contentId = [command argumentAtIndex:2];
235
- BOOL isFullscreen = [[command argumentAtIndex:3] boolValue];
236
-
237
- UIViewController *ctrl = [Purchasely planControllerFor:planVendorId with:presentationVendorId contentId:contentId loaded:nil completion:^(enum PLYProductViewControllerResult result, PLYPlan * _Nullable plan) {
238
- NSDictionary *resultDict = [self resultDictionaryForPresentationController:result plan:plan];
239
- [self successFor:command resultDict:resultDict];
266
+ __weak CDVPurchasely *weakSelf = self;
267
+ NSString *callbackId = command.callbackId;
268
+ builder = [builder onPresented:^(id<PLYPresentation> _Nullable presentation, NSError * _Nullable error) {
269
+ CDVPurchasely *strongSelf = weakSelf; if (strongSelf == nil) return;
270
+ strongSelf.currentPresentation = presentation;
271
+ NSMutableDictionary *env = [NSMutableDictionary new];
272
+ env[@"event"] = @"presented";
273
+ if (presentation != nil) { env[@"presentation"] = [strongSelf resultDictionaryForFetchPresentation:presentation]; }
274
+ if (error != nil) { env[@"error"] = error.localizedDescription; }
275
+ CDVPluginResult *r = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:env];
276
+ [r setKeepCallbackAsBool:YES];
277
+ [strongSelf.commandDelegate sendPluginResult:r callbackId:callbackId];
278
+ }];
279
+ builder = [builder onCloseRequested:^{
280
+ CDVPurchasely *strongSelf = weakSelf; if (strongSelf == nil) return;
281
+ CDVPluginResult *r = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:@{@"event": @"closeRequested"}];
282
+ [r setKeepCallbackAsBool:YES];
283
+ [strongSelf.commandDelegate sendPluginResult:r callbackId:callbackId];
284
+ }];
285
+ builder = [builder onDismissed:^(PLYPresentationOutcome * _Nonnull outcome) {
286
+ CDVPurchasely *strongSelf = weakSelf; if (strongSelf == nil) return;
287
+ strongSelf.currentPresentation = nil;
288
+ [strongSelf successFor:command resultDict:[strongSelf resultDictionaryForOutcome:outcome]];
240
289
  }];
241
290
 
242
- if (ctrl != nil) {
243
- UINavigationController *navCtrl = [[UINavigationController alloc] initWithRootViewController:ctrl];
244
- [navCtrl.navigationBar setTranslucent:YES];
245
- [navCtrl.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
246
- [navCtrl.navigationBar setShadowImage: [UIImage new]];
247
- [navCtrl.navigationBar setTintColor: [UIColor whiteColor]];
248
-
249
- self.presentedPresentationViewController = navCtrl;
250
-
251
- if (isFullscreen) {
252
- navCtrl.modalPresentationStyle = UIModalPresentationFullScreen;
253
- }
254
- [Purchasely showController:navCtrl type: PLYUIControllerTypeProductPage from:nil];
255
- }
291
+ id<PLYPresentationRequest> request = [builder build];
292
+ [request displayWithTransition:[self displayModeFromTransition:transition] completion:nil];
256
293
  }
257
294
 
258
- - (void)presentProductWithIdentifier:(CDVInvokedUrlCommand*)command {
259
- NSString *productVendorId = [command argumentAtIndex:0];
260
- NSString *presentationVendorId = [command argumentAtIndex:1];
261
- NSString *contentId = [command argumentAtIndex:2];
262
- BOOL isFullscreen = [[command argumentAtIndex:3] boolValue];
295
+ - (void)presentPresentationWithIdentifier:(CDVInvokedUrlCommand*)command {
296
+ NSString *presentationVendorId = [command argumentAtIndex:0];
297
+ NSString *contentId = [command argumentAtIndex:1];
298
+ id transition = [command argumentAtIndex:2];
263
299
 
264
- UIViewController *ctrl = [Purchasely productControllerFor:productVendorId with:presentationVendorId contentId:contentId loaded:nil completion:^(enum PLYProductViewControllerResult result, PLYPlan * _Nullable plan) {
265
- NSDictionary *resultDict = [self resultDictionaryForPresentationController:result plan:plan];
266
- [self successFor:command resultDict:resultDict];
267
- }];
300
+ if (![presentationVendorId isKindOfClass:[NSString class]]) {
301
+ [self failureFor:command resultString:@"presentationId is required"];
302
+ return;
303
+ }
268
304
 
269
- if (ctrl != nil) {
270
- UINavigationController *navCtrl = [[UINavigationController alloc] initWithRootViewController:ctrl];
271
- [navCtrl.navigationBar setTranslucent:YES];
272
- [navCtrl.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
273
- [navCtrl.navigationBar setShadowImage: [UIImage new]];
274
- [navCtrl.navigationBar setTintColor: [UIColor whiteColor]];
305
+ dispatch_async(dispatch_get_main_queue(), ^{
306
+ [self displayWithBuilder:[PLYPresentationBuilder forScreenId:presentationVendorId] contentId:contentId transition:transition command:command];
307
+ });
308
+ }
275
309
 
276
- self.presentedPresentationViewController = navCtrl;
310
+ - (void)presentPresentationForPlacement:(CDVInvokedUrlCommand*)command {
311
+ NSString *placementVendorId = [command argumentAtIndex:0];
312
+ NSString *contentId = [command argumentAtIndex:1];
313
+ id transition = [command argumentAtIndex:2];
277
314
 
278
- if (isFullscreen) {
279
- navCtrl.modalPresentationStyle = UIModalPresentationFullScreen;
280
- }
281
- [Purchasely showController:navCtrl type: PLYUIControllerTypeProductPage from:nil];
315
+ if (![placementVendorId isKindOfClass:[NSString class]]) {
316
+ [self failureFor:command resultString:@"placementId is required"];
317
+ return;
282
318
  }
319
+
320
+ dispatch_async(dispatch_get_main_queue(), ^{
321
+ [self displayWithBuilder:[PLYPresentationBuilder forPlacementId:placementVendorId] contentId:contentId transition:transition command:command];
322
+ });
283
323
  }
284
324
 
285
- - (void)presentSubscriptions:(CDVInvokedUrlCommand*)command {
286
- UIViewController *ctrl = [Purchasely subscriptionsController];
287
- UINavigationController *navCtrl = [[UINavigationController alloc] initWithRootViewController:ctrl];
288
- ctrl.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemDone target:navCtrl action:@selector(close)];
325
+ // v6: present the default (audience-targeted) presentation — no placement/screen id.
326
+ - (void)presentPresentationForDefault:(CDVInvokedUrlCommand*)command {
327
+ NSString *contentId = [command argumentAtIndex:0];
328
+ id transition = [command argumentAtIndex:1];
289
329
 
290
- [Purchasely showController:navCtrl type: PLYUIControllerTypeSubscriptionList from:nil];
330
+ dispatch_async(dispatch_get_main_queue(), ^{
331
+ [self displayWithBuilder:[[PLYPresentationBuilder alloc] init] contentId:contentId transition:transition command:command];
332
+ });
291
333
  }
292
334
 
293
335
  - (void)purchaseWithPlanVendorId:(CDVInvokedUrlCommand*)command {
@@ -358,7 +400,9 @@
358
400
 
359
401
  - (void)synchronize:(CDVInvokedUrlCommand*)command {
360
402
  [Purchasely synchronizeWithSuccess:^{
403
+ [self successFor:command resultBool:YES];
361
404
  } failure:^(NSError * _Nonnull error) {
405
+ [self failureFor:command resultString:error.localizedDescription];
362
406
  }];
363
407
  }
364
408
 
@@ -444,12 +488,14 @@
444
488
  }
445
489
 
446
490
  - (void)removeEventsListener:(CDVInvokedUrlCommand*)command {
447
- [Purchasely setEventDelegate:nil];
491
+ // v6 `setEventDelegate:` is _Nonnull (no native unregister). The delegate stays
492
+ // registered; clearing eventCommand makes `eventTriggered:` a no-op.
448
493
  self.eventCommand = nil;
449
494
  }
450
495
 
451
496
  - (void)removeUserAttributeListener:(CDVInvokedUrlCommand*)command {
452
- [Purchasely setUserAttributeDelegate:nil];
497
+ // v6 `setUserAttributeDelegate:` is _Nonnull (no native unregister). Clearing
498
+ // attributeCommand makes the user-attribute callbacks a no-op.
453
499
  self.attributeCommand = nil;
454
500
  }
455
501
 
@@ -458,12 +504,12 @@
458
504
  self.attributeCommand = command;
459
505
  }
460
506
 
461
- - (void)isDeeplinkHandled:(CDVInvokedUrlCommand*)command {
507
+ - (void)handleDeeplink:(CDVInvokedUrlCommand*)command {
462
508
  NSString *deeplinkString = [command argumentAtIndex:0];
463
509
  NSURL *deeplink = [NSURL URLWithString:deeplinkString];
464
510
 
465
511
  if (deeplink != nil) {
466
- BOOL result = [Purchasely isDeeplinkHandledWithDeeplink:deeplink];
512
+ BOOL result = [Purchasely handleDeeplink:deeplink];
467
513
  [self successFor:command resultBool:result];
468
514
  } else {
469
515
  [self successFor:command resultBool:NO];
@@ -509,33 +555,139 @@
509
555
 
510
556
  // Helpers
511
557
 
512
- - (NSDictionary<NSString *, NSObject *> *) resultDictionaryForPresentationController:(PLYProductViewControllerResult)result plan:(PLYPlan * _Nullable)plan {
513
- NSMutableDictionary<NSString *, NSObject *> *productViewResult = [NSMutableDictionary new];
514
- int resultString;
558
+ // v6: builds a PLYTransition from the JS transition object (see normalizeTransition):
559
+ // { type, dismissible?, height?: { type: 'pixel'|'percentage', value }, backgroundColor? }
560
+ // (a bare mode string is also tolerated). Objective-C only exposes percentage height for
561
+ // drawer/popin (pixel height and popin width are Swift-only in the SDK), so those honor a
562
+ // percentage height + dismissible + background colors; other dimensions are ignored.
563
+ // Returns nil for an unknown/absent value so the backend-defined default is honored.
564
+ - (PLYTransition * _Nullable) displayModeFromTransition:(id) transition {
565
+ NSDictionary *map = [transition isKindOfClass:[NSDictionary class]] ? transition : nil;
566
+ NSString *type = map != nil ? map[@"type"] : ([transition isKindOfClass:[NSString class]] ? transition : nil);
567
+ if (![type isKindOfClass:[NSString class]]) {
568
+ return nil;
569
+ }
570
+
571
+ BOOL dismissible = YES;
572
+ if ([map[@"dismissible"] isKindOfClass:[NSNumber class]]) {
573
+ dismissible = [map[@"dismissible"] boolValue];
574
+ }
515
575
 
516
- switch (result) {
517
- case PLYProductViewControllerResultPurchased:
518
- resultString = PLYProductViewControllerResultPurchased;
576
+ // Percentage height only (ObjC-accessible). Pixel height / popin width are not
577
+ // exposed to Objective-C by the SDK; a percentage value is expected in 0.0–1.0.
578
+ NSNumber *heightPercentage = nil;
579
+ NSDictionary *height = map[@"height"];
580
+ if ([height isKindOfClass:[NSDictionary class]] &&
581
+ [height[@"type"] isEqual:@"percentage"] &&
582
+ [height[@"value"] isKindOfClass:[NSNumber class]]) {
583
+ heightPercentage = height[@"value"];
584
+ }
585
+
586
+ PLYColors *backgroundColors = nil;
587
+ NSString *bgHex = map[@"backgroundColor"];
588
+ if ([bgHex isKindOfClass:[NSString class]]) {
589
+ UIColor *c = [UIColor ply_fromHex:bgHex];
590
+ if (c != nil) {
591
+ backgroundColors = [[PLYColors alloc] initWithLightColor:c darkColor:c];
592
+ }
593
+ }
594
+
595
+ if ([type isEqualToString:@"fullScreen"]) {
596
+ return [PLYTransition fullScreen];
597
+ } else if ([type isEqualToString:@"modal"]) {
598
+ return [PLYTransition modalWithDismissible:dismissible];
599
+ } else if ([type isEqualToString:@"push"]) {
600
+ return [PLYTransition push];
601
+ } else if ([type isEqualToString:@"inlinePaywall"]) {
602
+ return [PLYTransition inlinePaywall];
603
+ } else if ([type isEqualToString:@"drawer"]) {
604
+ return [[PLYTransition alloc] initWithType:PLYTransitionTypeDrawer heightPercentage:heightPercentage backgroundColors:backgroundColors dismissible:dismissible];
605
+ } else if ([type isEqualToString:@"popin"]) {
606
+ return [[PLYTransition alloc] initWithType:PLYTransitionTypePopin heightPercentage:heightPercentage backgroundColors:backgroundColors dismissible:dismissible];
607
+ }
608
+ return nil;
609
+ }
610
+
611
+ // v6: the dismiss outcome now arrives as a PLYPresentationOutcome (replaces the
612
+ // (PLYProductViewControllerResult, PLYPlan) pair). Serialized per the JS↔native contract.
613
+ - (NSDictionary<NSString *, NSObject *> *) resultDictionaryForOutcome:(PLYPresentationOutcome * _Nonnull)outcome {
614
+ NSMutableDictionary<NSString *, NSObject *> *dict = [NSMutableDictionary new];
615
+
616
+ // Map the v6 PLYPurchaseResult (cancelled=0, purchased=1, restored=2, none=3) to the
617
+ // JS PurchaseResult enum (PURCHASED=0, CANCELLED=1, RESTORED=2) for back-compat.
618
+ int result;
619
+ switch (outcome.purchaseResult) {
620
+ case PLYPurchaseResultPurchased:
621
+ result = 0;
622
+ break;
623
+ case PLYPurchaseResultCancelled:
624
+ result = 1;
519
625
  break;
520
- case PLYProductViewControllerResultRestored:
521
- resultString = PLYProductViewControllerResultRestored;
626
+ case PLYPurchaseResultRestored:
627
+ result = 2;
522
628
  break;
523
- case PLYProductViewControllerResultCancelled:
524
- resultString = PLYProductViewControllerResultCancelled;
629
+ case PLYPurchaseResultNone:
630
+ // TODO(v6-verify): PLYPurchaseResultNone (no purchase action) has no JS PurchaseResult
631
+ // equivalent; mapped to CANCELLED(1). closeReason still conveys the precise reason.
632
+ result = 1;
525
633
  break;
526
634
  }
635
+ [dict setObject:[NSNumber numberWithInt:result] forKey:@"result"];
636
+
637
+ // v6: also expose the string purchaseResult (matches the Flutter outcome contract);
638
+ // omitted for PLYPurchaseResultNone (no purchase action).
639
+ NSString *purchaseResultString = nil;
640
+ switch (outcome.purchaseResult) {
641
+ case PLYPurchaseResultPurchased: purchaseResultString = @"purchased"; break;
642
+ case PLYPurchaseResultCancelled: purchaseResultString = @"cancelled"; break;
643
+ case PLYPurchaseResultRestored: purchaseResultString = @"restored"; break;
644
+ case PLYPurchaseResultNone: break;
645
+ }
646
+ if (purchaseResultString != nil) {
647
+ [dict setObject:purchaseResultString forKey:@"purchaseResult"];
648
+ }
527
649
 
528
- [productViewResult setObject:[NSNumber numberWithInt:resultString] forKey:@"result"];
650
+ if (outcome.plan != nil) {
651
+ [dict setObject:[outcome.plan asDictionary] forKey:@"plan"];
652
+ }
653
+
654
+ // closeReason strings match the JS Purchasely.CloseReason enum, aligned with the
655
+ // native PLYCloseReason wire contract shared with Flutter (button / back_system /
656
+ // programmatic). iOS's interactive (swipe) dismiss maps onto `back_system`; a close
657
+ // with no dismiss reason (PLYCloseReasonNone, e.g. after a purchase) omits the key,
658
+ // mirroring the null closeReason the Flutter bridge reports on iOS.
659
+ NSString *closeReason = nil;
660
+ switch (outcome.closeReason) {
661
+ case PLYCloseReasonButton:
662
+ closeReason = @"button";
663
+ break;
664
+ case PLYCloseReasonInteractiveDismiss:
665
+ closeReason = @"back_system";
666
+ break;
667
+ case PLYCloseReasonProgrammatic:
668
+ closeReason = @"programmatic";
669
+ break;
670
+ case PLYCloseReasonNone:
671
+ break;
672
+ }
673
+ if (closeReason != nil) {
674
+ [dict setObject:closeReason forKey:@"closeReason"];
675
+ }
676
+
677
+ if (outcome.error != nil) {
678
+ [dict setObject:outcome.error.localizedDescription forKey:@"error"];
679
+ }
529
680
 
530
- if (plan != nil) {
531
- [productViewResult setObject:[plan asDictionary] forKey:@"plan"];
681
+ if (outcome.presentation != nil) {
682
+ [dict setObject:[self resultDictionaryForFetchPresentation:outcome.presentation] forKey:@"presentation"];
532
683
  }
533
- return productViewResult;
684
+
685
+ return dict;
534
686
  }
535
687
 
536
688
  - (NSDictionary<NSString *, NSObject *> *) resultDictionaryForActionInterceptor:(PLYPresentationAction) action
537
689
  parameters: (PLYPresentationActionParameters * _Nullable) params
538
- presentationInfos: (PLYPresentationInfo * _Nullable) infos {
690
+ info: (PLYInterceptorInfo * _Nullable) info {
539
691
  NSMutableDictionary<NSString *, NSObject *> *actionInterceptorResult = [NSMutableDictionary new];
540
692
 
541
693
  NSString* actionString;
@@ -575,22 +727,27 @@
575
727
 
576
728
  [actionInterceptorResult setObject:actionString forKey:@"action"];
577
729
 
578
- if (infos != nil) {
730
+ // v6: PLYPresentationInfo was replaced by PLYInterceptorInfo. The flat id fields
731
+ // (presentationId/placementId/abTestId/…) are now accessed through info.presentation.
732
+ if (info != nil) {
579
733
  NSMutableDictionary<NSString *, NSObject *> *infosResult = [NSMutableDictionary new];
580
- if (infos.contentId != nil) {
581
- [infosResult setObject:infos.contentId forKey:@"contentId"];
582
- }
583
- if (infos.presentationId != nil) {
584
- [infosResult setObject:infos.presentationId forKey:@"presentationId"];
585
- }
586
- if (infos.placementId != nil) {
587
- [infosResult setObject:infos.placementId forKey:@"placementId"];
588
- }
589
- if (infos.abTestId != nil) {
590
- [infosResult setObject:infos.abTestId forKey:@"abTestId"];
734
+ if (info.contentId != nil) {
735
+ [infosResult setObject:info.contentId forKey:@"contentId"];
591
736
  }
592
- if (infos.abTestVariantId != nil) {
593
- [infosResult setObject:infos.abTestVariantId forKey:@"abTestVariantId"];
737
+ id<PLYPresentation> presentation = info.presentation;
738
+ if (presentation != nil) {
739
+ if (presentation.screenId != nil) {
740
+ [infosResult setObject:presentation.screenId forKey:@"presentationId"];
741
+ }
742
+ if (presentation.placementId != nil) {
743
+ [infosResult setObject:presentation.placementId forKey:@"placementId"];
744
+ }
745
+ if (presentation.abTestId != nil) {
746
+ [infosResult setObject:presentation.abTestId forKey:@"abTestId"];
747
+ }
748
+ if (presentation.abTestVariantId != nil) {
749
+ [infosResult setObject:presentation.abTestVariantId forKey:@"abTestVariantId"];
750
+ }
594
751
  }
595
752
 
596
753
  [actionInterceptorResult setObject:infosResult forKey:@"info"];
@@ -609,6 +766,9 @@
609
766
  if (params.presentation != nil) {
610
767
  [paramsResult setObject:params.presentation forKey:@"presentation"];
611
768
  }
769
+ if (params.placement != nil) {
770
+ [paramsResult setObject:params.placement forKey:@"placementId"];
771
+ }
612
772
  if (params.promoOffer != nil) {
613
773
  NSMutableDictionary<NSString *, NSObject *> *promoOffer = [NSMutableDictionary new];
614
774
  [promoOffer setObject:params.promoOffer.vendorId forKey:@"vendorId"];
@@ -635,58 +795,112 @@ static NSString * PLYWebCheckoutProviderToString(PLYWebCheckoutProvider provider
635
795
  return @"stripe";
636
796
  case PLYWebCheckoutProviderOther:
637
797
  return @"other";
798
+ case PLYWebCheckoutProviderNone:
799
+ return @"none";
638
800
  default:
639
801
  return @"unknown";
640
802
  }
641
803
  }
642
804
 
643
- - (void)setPaywallActionInterceptor:(CDVInvokedUrlCommand*)command {
644
- self.paywallActionInterceptorCommand = command;
645
- [Purchasely setPaywallActionsInterceptor:^(enum PLYPresentationAction action, PLYPresentationActionParameters * _Nullable parameters, PLYPresentationInfo * _Nullable infos, void (^ _Nonnull onProcessActionHandler)(BOOL)) {
646
- self.onProcessActionHandler = onProcessActionHandler;
647
- NSDictionary *resultDict = [self resultDictionaryForActionInterceptor:action parameters:parameters presentationInfos:infos];
805
+ // Maps a JS action-kind string (see Purchasely.PresentationAction) to a PLYPresentationAction.
806
+ // Returns NO for an unknown kind.
807
+ static BOOL PLYPresentationActionFromString(NSString *kind, PLYPresentationAction *out) {
808
+ static NSDictionary<NSString *, NSNumber *> *map;
809
+ static dispatch_once_t once;
810
+ dispatch_once(&once, ^{
811
+ map = @{
812
+ @"login": @(PLYPresentationActionLogin),
813
+ @"purchase": @(PLYPresentationActionPurchase),
814
+ @"close": @(PLYPresentationActionClose),
815
+ @"close_all": @(PLYPresentationActionCloseAll),
816
+ @"restore": @(PLYPresentationActionRestore),
817
+ @"navigate": @(PLYPresentationActionNavigate),
818
+ @"promo_code": @(PLYPresentationActionPromoCode),
819
+ @"open_presentation": @(PLYPresentationActionOpenPresentation),
820
+ @"open_placement": @(PLYPresentationActionOpenPlacement),
821
+ @"web_checkout": @(PLYPresentationActionWebCheckout),
822
+ };
823
+ });
824
+ NSNumber *value = [kind isKindOfClass:[NSString class]] ? map[kind] : nil;
825
+ if (value == nil) return NO;
826
+ if (out != NULL) *out = (PLYPresentationAction)value.integerValue;
827
+ return YES;
828
+ }
829
+
830
+ // v6: register a JS handler for a single action kind. Each intercept emits an
831
+ // event carrying a unique `callbackId`; JS replies via completeActionInterceptor.
832
+ - (void)registerActionInterceptor:(CDVInvokedUrlCommand*)command {
833
+ NSString *kind = [command argumentAtIndex:0];
834
+ PLYPresentationAction action;
835
+ if (!PLYPresentationActionFromString(kind, &action)) {
836
+ NSLog(@"[Purchasely] unknown interceptor kind: %@", kind);
837
+ return;
838
+ }
839
+ self.actionInterceptorCallbackIds[kind] = command.callbackId;
648
840
 
649
- CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:resultDict];
650
- [pluginResult setKeepCallbackAsBool:YES];
651
- [self.commandDelegate sendPluginResult:pluginResult callbackId:self.paywallActionInterceptorCommand.callbackId];
841
+ __weak CDVPurchasely *weakSelf = self;
842
+ [Purchasely interceptAction:action handler:^(PLYInterceptorInfo * _Nonnull info, PLYPresentationActionParameters * _Nullable params, void (^ _Nonnull completion)(enum PLYInterceptResult)) {
843
+ CDVPurchasely *strongSelf = weakSelf;
844
+ if (strongSelf == nil) { completion(PLYInterceptResultNotHandled); return; }
652
845
 
846
+ NSString *callbackId = strongSelf.actionInterceptorCallbackIds[kind];
847
+ if (callbackId == nil) { completion(PLYInterceptResultNotHandled); return; }
848
+
849
+ // Unique id per intercepted invocation so concurrent intercepts resolve independently.
850
+ NSString *invocationId = [NSString stringWithFormat:@"%@#%lu", kind, (unsigned long)(++strongSelf.interceptorInvocationCounter)];
851
+ // Copy the escaping completion onto the heap before stashing (the dictionary retains but
852
+ // does not copy; the old single-slot property was declared `copy`).
853
+ strongSelf.pendingInterceptCompletions[invocationId] = [completion copy];
854
+
855
+ NSMutableDictionary *event = [[strongSelf resultDictionaryForActionInterceptor:action parameters:params info:info] mutableCopy];
856
+ event[@"callbackId"] = invocationId;
857
+
858
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:event];
859
+ [pluginResult setKeepCallbackAsBool:YES];
860
+ [strongSelf.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
653
861
  }];
654
862
  }
655
863
 
656
- - (void)onProcessAction:(CDVInvokedUrlCommand*)command {
657
- BOOL processAction = [[command argumentAtIndex:0] boolValue];
658
- if (self.onProcessActionHandler != nil) {
659
- self.onProcessActionHandler(processAction);
660
- }
864
+ // v6: stop intercepting a single action kind.
865
+ - (void)unregisterActionInterceptor:(CDVInvokedUrlCommand*)command {
866
+ NSString *kind = [command argumentAtIndex:0];
867
+ PLYPresentationAction action;
868
+ if (!PLYPresentationActionFromString(kind, &action)) return;
869
+ [self.actionInterceptorCallbackIds removeObjectForKey:kind];
870
+ [Purchasely removeActionInterceptor:action];
661
871
  }
662
872
 
663
- - (void)closePresentation:(CDVInvokedUrlCommand*)command {
664
- if (self.presentedPresentationViewController != nil) {
665
- dispatch_async(dispatch_get_main_queue(), ^{
666
- [self.presentedPresentationViewController dismissViewControllerAnimated:true completion:^{
667
- self.presentedPresentationViewController = nil;
668
- self.shouldReopenPaywall = NO;
669
- }];
670
- });
873
+ // v6: JS reports how an intercepted action was handled, keyed by the invocation
874
+ // id carried on the intercept event. Result is "success"/"failed"/"notHandled".
875
+ - (void)completeActionInterceptor:(CDVInvokedUrlCommand*)command {
876
+ NSString *invocationId = [command argumentAtIndex:0];
877
+ NSString *resultString = [command argumentAtIndex:1];
878
+ if (![invocationId isKindOfClass:[NSString class]]) return;
879
+
880
+ void (^completion)(enum PLYInterceptResult) = self.pendingInterceptCompletions[invocationId];
881
+ if (completion == nil) return;
882
+ [self.pendingInterceptCompletions removeObjectForKey:invocationId];
883
+
884
+ enum PLYInterceptResult result = PLYInterceptResultNotHandled;
885
+ if ([resultString isEqualToString:@"success"]) {
886
+ result = PLYInterceptResultSuccess;
887
+ } else if ([resultString isEqualToString:@"failed"]) {
888
+ result = PLYInterceptResultFailed;
671
889
  }
890
+ completion(result);
672
891
  }
673
892
 
674
- - (void)hidePresentation:(CDVInvokedUrlCommand*)command {
675
- if (self.presentedPresentationViewController != nil) {
676
- dispatch_async(dispatch_get_main_queue(), ^{
677
- [self.presentedPresentationViewController dismissViewControllerAnimated:true completion:^{ }];
678
- self.shouldReopenPaywall = YES;
679
- });
680
- }
893
+ - (void)closePresentation:(CDVInvokedUrlCommand*)command {
894
+ dispatch_async(dispatch_get_main_queue(), ^{
895
+ [Purchasely closeAllScreens];
896
+ self.currentPresentation = nil;
897
+ });
681
898
  }
682
899
 
683
- - (void)showPresentation:(CDVInvokedUrlCommand*)command {
900
+ - (void)backPresentation:(CDVInvokedUrlCommand*)command {
684
901
  dispatch_async(dispatch_get_main_queue(), ^{
685
- if (self.presentedPresentationViewController && self.shouldReopenPaywall) {
686
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
687
- self.shouldReopenPaywall = NO;
688
- [Purchasely showController:self.presentedPresentationViewController type:PLYUIControllerTypeProductPage from:nil];
689
- });
902
+ if (self.currentPresentation != nil) {
903
+ [self.currentPresentation back];
690
904
  }
691
905
  });
692
906
  }
@@ -887,84 +1101,96 @@ static NSString * PLYWebCheckoutProviderToString(PLYWebCheckoutProvider provider
887
1101
  NSString *contentId = [command argumentAtIndex:2];
888
1102
 
889
1103
  dispatch_async(dispatch_get_main_queue(), ^{
890
- if (placementId != nil) {
891
- [Purchasely fetchPresentationFor:placementId contentId: contentId fetchCompletion:^(PLYPresentation * _Nullable presentation, NSError * _Nullable error) {
892
- if (error != nil) {
893
- [self failureFor:command resultString: error.localizedDescription];
894
- } else if (presentation != nil) {
895
- [self.presentationsLoaded addObject:presentation];
896
- [self successFor:command resultDict:[self resultDictionaryForFetchPresentation:presentation]];
897
- }
898
- } completion:^(enum PLYProductViewControllerResult result, PLYPlan * _Nullable plan) {
899
- if (self.purchaseResolve != nil){
900
- [self successFor:self.purchaseResolve resultDict:[self resultDictionaryForPresentationController:result plan:plan]];
901
- }
902
- } loadedCompletion:nil];
903
- } else {
904
- [Purchasely fetchPresentationWith:presentationId contentId: contentId fetchCompletion:^(PLYPresentation * _Nullable presentation, NSError * _Nullable error) {
905
- if (error != nil) {
906
- [self failureFor:command resultString: error.localizedDescription];
907
- } else if (presentation != nil) {
908
- [self.presentationsLoaded addObject:presentation];
909
- [self successFor:command resultDict:[self resultDictionaryForFetchPresentation:presentation]];
910
- }
911
- } completion:^(enum PLYProductViewControllerResult result, PLYPlan * _Nullable plan) {
912
- if (self.purchaseResolve != nil) {
913
- [self successFor:self.purchaseResolve resultDict:[self resultDictionaryForPresentationController:result plan:plan]];
914
- }
915
- } loadedCompletion:nil];
916
- }
917
- });
1104
+ PLYPresentationBuilder *builder;
1105
+ if ([placementId isKindOfClass:[NSString class]]) {
1106
+ builder = [PLYPresentationBuilder forPlacementId:placementId];
1107
+ } else if ([presentationId isKindOfClass:[NSString class]]) {
1108
+ builder = [PLYPresentationBuilder forScreenId:presentationId];
1109
+ } else {
1110
+ // Neither id provided → the default (audience-targeted) presentation.
1111
+ builder = [[PLYPresentationBuilder alloc] init];
1112
+ }
1113
+
1114
+ if ([contentId isKindOfClass:[NSString class]]) {
1115
+ builder = [builder contentId:contentId];
1116
+ }
1117
+
1118
+ // v6: build a request and preload it (fetch without display). The purchase/dismiss
1119
+ // outcome now flows through the later presentPresentation: display, not fetch.
1120
+ id<PLYPresentationRequest> request = [builder build];
1121
+ [request preloadWithCompletion:^(id<PLYPresentation> _Nullable presentation, NSError * _Nullable error) {
1122
+ if (error != nil) {
1123
+ [self failureFor:command resultString: error.localizedDescription];
1124
+ } else if (presentation != nil) {
1125
+ [self.presentationsLoaded addObject:presentation];
1126
+ [self successFor:command resultDict:[self resultDictionaryForFetchPresentation:presentation]];
1127
+ }
1128
+ }];
1129
+ });
918
1130
  }
919
1131
 
920
1132
  - (void)presentPresentation:(CDVInvokedUrlCommand *)command {
921
1133
  NSDictionary<NSString *, id> *presentationDictionary = [command argumentAtIndex:0];
922
- BOOL isFullscreen = [[command argumentAtIndex:1] boolValue];
1134
+ id transition = [command argumentAtIndex:1];
923
1135
  NSString *loadingBackgroundColor = [command argumentAtIndex:2];
924
1136
 
925
1137
  if (presentationDictionary == nil) {
926
- [self failureFor:command resultString: @"Presentation cannot be null"];
927
- return;
928
- }
929
-
930
- self.purchaseResolve = command;
931
-
932
- dispatch_async(dispatch_get_main_queue(), ^{
933
-
934
- PLYPresentation *presentationLoaded = [self findPresentationLoadedFor:(NSString *)[presentationDictionary objectForKey:@"id"]];
1138
+ [self failureFor:command resultString: @"Presentation cannot be null"];
1139
+ return;
1140
+ }
935
1141
 
936
- if (presentationLoaded == nil || presentationLoaded.controller == nil) {
937
- [self failureFor:command resultString: @"Presentation not loaded"];
938
- return;
939
- }
1142
+ self.purchaseResolve = command;
940
1143
 
941
- [self.presentationsLoaded removeObjectAtIndex:[self findIndexPresentationLoadedFor:(NSString *)[presentationDictionary objectForKey:@"id"]]];
1144
+ dispatch_async(dispatch_get_main_queue(), ^{
1145
+ NSString *presentationId = (NSString *)[presentationDictionary objectForKey:@"id"];
1146
+ id<PLYPresentation> presentationLoaded = [self findPresentationLoadedFor:presentationId];
942
1147
 
943
- if (presentationLoaded.controller != nil) {
944
- if (loadingBackgroundColor != nil) {
945
- UIColor *backColor = [UIColor ply_fromHex:loadingBackgroundColor];
946
- if (backColor != nil) {
947
- [presentationLoaded.controller.view setBackgroundColor:backColor];
948
- }
949
- }
1148
+ if (presentationLoaded == nil || presentationLoaded.controller == nil) {
1149
+ [self failureFor:command resultString: @"Presentation not loaded"];
1150
+ return;
1151
+ }
950
1152
 
951
- if (isFullscreen) {
952
- presentationLoaded.controller.modalPresentationStyle = UIModalPresentationFullScreen;
953
- }
1153
+ NSInteger index = [self findIndexPresentationLoadedFor:presentationId];
1154
+ if (index >= 0) {
1155
+ [self.presentationsLoaded removeObjectAtIndex:index];
1156
+ }
954
1157
 
955
- self.shouldReopenPaywall = NO;
956
-
957
- [Purchasely closeDisplayedPresentation];
958
- self.presentedPresentationViewController = presentationLoaded.controller;
959
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
960
- if (presentationLoaded.isFlow) {
961
- [presentationLoaded displayFrom:nil];
962
- } else {
963
- [Purchasely showController:presentationLoaded.controller type: PLYUIControllerTypeProductPage from:nil];
964
- }
965
- });
1158
+ if (loadingBackgroundColor != nil) {
1159
+ UIColor *backColor = [UIColor ply_fromHex:loadingBackgroundColor];
1160
+ if (backColor != nil) {
1161
+ [presentationLoaded.controller.view setBackgroundColor:backColor];
966
1162
  }
967
- });
1163
+ }
1164
+
1165
+ // Stream the presentation lifecycle to this command (keep-alive envelopes for
1166
+ // presented/closeRequested, final outcome on dismiss).
1167
+ __weak CDVPurchasely *weakSelf = self;
1168
+ NSString *callbackId = command.callbackId;
1169
+ presentationLoaded.onPresented = ^(id<PLYPresentation> _Nullable presentation, NSError * _Nullable error) {
1170
+ CDVPurchasely *strongSelf = weakSelf; if (strongSelf == nil) return;
1171
+ NSMutableDictionary *env = [NSMutableDictionary new];
1172
+ env[@"event"] = @"presented";
1173
+ if (presentation != nil) { env[@"presentation"] = [strongSelf resultDictionaryForFetchPresentation:presentation]; }
1174
+ if (error != nil) { env[@"error"] = error.localizedDescription; }
1175
+ CDVPluginResult *r = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:env];
1176
+ [r setKeepCallbackAsBool:YES];
1177
+ [strongSelf.commandDelegate sendPluginResult:r callbackId:callbackId];
1178
+ };
1179
+ presentationLoaded.onCloseRequested = ^{
1180
+ CDVPurchasely *strongSelf = weakSelf; if (strongSelf == nil) return;
1181
+ CDVPluginResult *r = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:@{@"event": @"closeRequested"}];
1182
+ [r setKeepCallbackAsBool:YES];
1183
+ [strongSelf.commandDelegate sendPluginResult:r callbackId:callbackId];
1184
+ };
1185
+ presentationLoaded.onDismissed = ^(PLYPresentationOutcome * _Nonnull outcome) {
1186
+ CDVPurchasely *strongSelf = weakSelf; if (strongSelf == nil) return;
1187
+ strongSelf.currentPresentation = nil;
1188
+ [strongSelf successFor:command resultDict:[strongSelf resultDictionaryForOutcome:outcome]];
1189
+ };
1190
+
1191
+ self.currentPresentation = presentationLoaded;
1192
+ [presentationLoaded displayFrom:nil transitionType:[self displayModeFromTransition:transition]];
1193
+ });
968
1194
  }
969
1195
 
970
1196
  - (void) revokeDataProcessingConsent:(CDVInvokedUrlCommand *)command {
@@ -996,9 +1222,9 @@ static NSString * PLYWebCheckoutProviderToString(PLYWebCheckoutProvider provider
996
1222
  [Purchasely setDebugModeWithEnabled: enabled];
997
1223
  }
998
1224
 
999
- - (PLYPresentation *) findPresentationLoadedFor:(NSString * _Nullable) presentationId {
1000
- for (PLYPresentation *presentationLoaded in self.presentationsLoaded) {
1001
- if ([presentationLoaded.id isEqualToString: presentationId]) {
1225
+ - (id<PLYPresentation>) findPresentationLoadedFor:(NSString * _Nullable) presentationId {
1226
+ for (id<PLYPresentation> presentationLoaded in self.presentationsLoaded) {
1227
+ if ([presentationLoaded.screenId isEqualToString: presentationId]) {
1002
1228
  return presentationLoaded;
1003
1229
  }
1004
1230
  }
@@ -1007,8 +1233,8 @@ static NSString * PLYWebCheckoutProviderToString(PLYWebCheckoutProvider provider
1007
1233
 
1008
1234
  - (NSInteger) findIndexPresentationLoadedFor:(NSString * _Nullable) presentationId {
1009
1235
  NSInteger index = 0;
1010
- for (PLYPresentation *presentationLoaded in self.presentationsLoaded) {
1011
- if ([presentationLoaded.id isEqualToString: presentationId]) {
1236
+ for (id<PLYPresentation> presentationLoaded in self.presentationsLoaded) {
1237
+ if ([presentationLoaded.screenId isEqualToString: presentationId]) {
1012
1238
  return index;
1013
1239
  }
1014
1240
  index++;
@@ -1038,14 +1264,13 @@ static NSString * PLYWebCheckoutProviderToString(PLYWebCheckoutProvider provider
1038
1264
  return dict;
1039
1265
  }
1040
1266
 
1041
- - (NSDictionary<NSString *, NSObject *> *) resultDictionaryForFetchPresentation:(PLYPresentation * _Nullable) presentation {
1267
+ - (NSDictionary<NSString *, NSObject *> *) resultDictionaryForFetchPresentation:(id<PLYPresentation> _Nullable) presentation {
1042
1268
  NSMutableDictionary<NSString *, NSObject *> *presentationResult = [NSMutableDictionary new];
1043
1269
 
1044
- // TODO: fill all parameters.
1045
1270
  if (presentation != nil) {
1046
1271
 
1047
- if (presentation.id != nil) {
1048
- [presentationResult setObject:presentation.id forKey:@"id"];
1272
+ if (presentation.screenId != nil) {
1273
+ [presentationResult setObject:presentation.screenId forKey:@"id"];
1049
1274
  }
1050
1275
 
1051
1276
  if (presentation.placementId != nil) {
@@ -1064,6 +1289,17 @@ static NSString * PLYWebCheckoutProviderToString(PLYWebCheckoutProvider provider
1064
1289
  [presentationResult setObject:presentation.abTestVariantId forKey:@"abTestVariantId"];
1065
1290
  }
1066
1291
 
1292
+ // New in v6: campaignId, flowId, height.
1293
+ if (presentation.campaignId != nil) {
1294
+ [presentationResult setObject:presentation.campaignId forKey:@"campaignId"];
1295
+ }
1296
+
1297
+ if (presentation.flowId != nil) {
1298
+ [presentationResult setObject:presentation.flowId forKey:@"flowId"];
1299
+ }
1300
+
1301
+ [presentationResult setObject:[NSNumber numberWithInteger:presentation.height] forKey:@"height"];
1302
+
1067
1303
  if (presentation.language != nil) {
1068
1304
  [presentationResult setObject:presentation.language forKey:@"language"];
1069
1305
  }