@purchasely/cordova-plugin-purchasely 6.0.0-rc.1 → 6.0.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/README.md +22 -14
- package/__tests__/Purchasely.test.js +820 -155
- package/example/IOS_8_1_1_TEST_MATRIX.md +25 -0
- package/example/config.xml +1 -1
- package/example/e2e/README.md +2 -2
- package/example/e2e/helpers/driver.js +135 -18
- package/example/e2e/specs/bridge.e2e.js +116 -13
- package/example/e2e/specs/dismiss.e2e.js +32 -12
- package/example/e2e/tools/ci_run_e2e.sh +15 -1
- package/example/e2e/tools/ci_run_e2e_ios.sh +12 -2
- package/example/e2e/wdio.android.conf.js +3 -0
- package/example/e2e/wdio.ios.conf.js +26 -1
- package/example/ios.sh +6 -5
- package/example/package-lock.json +17 -14
- package/example/package.json +4 -3
- package/example/www/js/index.js +62 -58
- package/package.json +1 -1
- package/plugin.xml +4 -3
- package/src/android/PurchaselyPlugin.kt +247 -67
- package/src/ios/CDVPurchasely+Events.m +7 -1
- package/src/ios/CDVPurchasely.h +11 -0
- package/src/ios/CDVPurchasely.m +174 -22
- package/src/ios/Hybrid/PLYPlan+Hybrid.m +19 -0
- package/src/ios/Hybrid/PLYSubscription+Hybrid.m +13 -0
- package/www/Purchasely.js +390 -46
|
@@ -47,6 +47,9 @@ describe('Purchasely', () => {
|
|
|
47
47
|
expect(Purchasely.Attribute.APPSFLYER_ID).toBe(5);
|
|
48
48
|
expect(Purchasely.Attribute.AMPLITUDE_USER_ID).toBe(16);
|
|
49
49
|
expect(Purchasely.Attribute.BATCH_CUSTOM_USER_ID).toBe(20);
|
|
50
|
+
// ENM-02 / REC-11
|
|
51
|
+
expect(Purchasely.Attribute.ONESIGNAL_USER_ID).toBe(21);
|
|
52
|
+
expect(Purchasely.Attribute.oneSignalPlayerId).toBeUndefined();
|
|
50
53
|
});
|
|
51
54
|
});
|
|
52
55
|
|
|
@@ -96,6 +99,14 @@ describe('Purchasely', () => {
|
|
|
96
99
|
});
|
|
97
100
|
});
|
|
98
101
|
|
|
102
|
+
describe('BillingPlanType', () => {
|
|
103
|
+
it('should have correct billing plan type values (iOS 26.4+ commitment, Apple only)', () => {
|
|
104
|
+
expect(Purchasely.BillingPlanType.unspecified).toBe(0);
|
|
105
|
+
expect(Purchasely.BillingPlanType.upFront).toBe(1);
|
|
106
|
+
expect(Purchasely.BillingPlanType.monthly).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
99
110
|
describe('RunningMode', () => {
|
|
100
111
|
it('should expose the Purchasely 6.0 running modes by name', () => {
|
|
101
112
|
expect(Purchasely.RunningMode.observer).toBe('observer');
|
|
@@ -106,15 +117,15 @@ describe('Purchasely', () => {
|
|
|
106
117
|
describe('PresentationAction', () => {
|
|
107
118
|
it('should have correct presentation action values', () => {
|
|
108
119
|
expect(Purchasely.PresentationAction.close).toBe('close');
|
|
109
|
-
expect(Purchasely.PresentationAction.
|
|
120
|
+
expect(Purchasely.PresentationAction.closeAll).toBe('close_all');
|
|
110
121
|
expect(Purchasely.PresentationAction.login).toBe('login');
|
|
111
122
|
expect(Purchasely.PresentationAction.navigate).toBe('navigate');
|
|
112
123
|
expect(Purchasely.PresentationAction.purchase).toBe('purchase');
|
|
113
124
|
expect(Purchasely.PresentationAction.restore).toBe('restore');
|
|
114
|
-
expect(Purchasely.PresentationAction.
|
|
115
|
-
expect(Purchasely.PresentationAction.
|
|
116
|
-
expect(Purchasely.PresentationAction.
|
|
117
|
-
expect(Purchasely.PresentationAction.
|
|
125
|
+
expect(Purchasely.PresentationAction.openPresentation).toBe('open_presentation');
|
|
126
|
+
expect(Purchasely.PresentationAction.openPlacement).toBe('open_placement');
|
|
127
|
+
expect(Purchasely.PresentationAction.promoCode).toBe('promo_code');
|
|
128
|
+
expect(Purchasely.PresentationAction.webCheckout).toBe('web_checkout');
|
|
118
129
|
});
|
|
119
130
|
});
|
|
120
131
|
|
|
@@ -228,9 +239,122 @@ describe('Purchasely', () => {
|
|
|
228
239
|
]
|
|
229
240
|
);
|
|
230
241
|
});
|
|
242
|
+
|
|
243
|
+
it('should fall back to the literal default sdkVersion when plugin_list metadata is missing', () => {
|
|
244
|
+
const metadata = global.cordova.define.moduleMap['cordova/plugin_list'].exports.metadata;
|
|
245
|
+
const original = metadata['cordova-plugin-purchasely'];
|
|
246
|
+
delete metadata['cordova-plugin-purchasely'];
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
Purchasely.start({ apiKey: 'API_KEY' }, jest.fn(), jest.fn());
|
|
250
|
+
|
|
251
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
252
|
+
expect.any(Function),
|
|
253
|
+
expect.any(Function),
|
|
254
|
+
'Purchasely',
|
|
255
|
+
'start',
|
|
256
|
+
[{ apiKey: 'API_KEY', sdkVersion: '6.0.0' }]
|
|
257
|
+
);
|
|
258
|
+
} finally {
|
|
259
|
+
metadata['cordova-plugin-purchasely'] = original;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should treat a v5-style positional call as broken (no longer supported)', () => {
|
|
264
|
+
// Purchasely 6.0: `start` takes a single options object. A v5-style positional
|
|
265
|
+
// call (apiKey, stores, storeKit1, ...) is NOT supported: `options` becomes the
|
|
266
|
+
// raw apiKey string (sdkVersion silently fails to attach to a string primitive),
|
|
267
|
+
// and `success`/`error` are mis-bound to the 2nd/3rd positional args instead of
|
|
268
|
+
// the real callbacks. This documents the breaking change from MIGRATION-v6.md.
|
|
269
|
+
const legacyStores = ['Google'];
|
|
270
|
+
const legacyStoreKit1 = false;
|
|
271
|
+
|
|
272
|
+
Purchasely.start('API_KEY', legacyStores, legacyStoreKit1, null, 0, 'full', jest.fn(), jest.fn());
|
|
273
|
+
|
|
274
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
275
|
+
legacyStores,
|
|
276
|
+
legacyStoreKit1,
|
|
277
|
+
'Purchasely',
|
|
278
|
+
'start',
|
|
279
|
+
['API_KEY']
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('builder(apiKey) (fluent alias of start)', () => {
|
|
285
|
+
it('accumulates chained options and delegates to exports.start with (success, error) callbacks', () => {
|
|
286
|
+
const success = jest.fn();
|
|
287
|
+
const error = jest.fn();
|
|
288
|
+
|
|
289
|
+
Purchasely.builder('API_KEY')
|
|
290
|
+
.appUserId('user123')
|
|
291
|
+
.runningMode(Purchasely.RunningMode.full)
|
|
292
|
+
.logLevel(Purchasely.LogLevel.INFO)
|
|
293
|
+
.allowDeeplink(true)
|
|
294
|
+
.allowCampaigns(false)
|
|
295
|
+
.stores([Purchasely.Store.google])
|
|
296
|
+
.storekitVersion(Purchasely.StorekitVersion.storeKit2)
|
|
297
|
+
.storeKit1(false)
|
|
298
|
+
.deeplink('myapp://deeplink')
|
|
299
|
+
.start(success, error);
|
|
300
|
+
|
|
301
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
302
|
+
success,
|
|
303
|
+
error,
|
|
304
|
+
'Purchasely',
|
|
305
|
+
'start',
|
|
306
|
+
[
|
|
307
|
+
{
|
|
308
|
+
apiKey: 'API_KEY',
|
|
309
|
+
appUserId: 'user123',
|
|
310
|
+
runningMode: 'full',
|
|
311
|
+
logLevel: 1,
|
|
312
|
+
allowDeeplink: true,
|
|
313
|
+
allowCampaigns: false,
|
|
314
|
+
stores: ['Google'],
|
|
315
|
+
storekitVersion: 'storeKit2',
|
|
316
|
+
storeKit1: false,
|
|
317
|
+
deeplink: 'myapp://deeplink',
|
|
318
|
+
sdkVersion: '5.6.2'
|
|
319
|
+
}
|
|
320
|
+
]
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('start() with no callbacks returns a Promise resolving the isConfigured value', async () => {
|
|
325
|
+
const startPromise = Purchasely.builder('API_KEY').start();
|
|
326
|
+
|
|
327
|
+
const call = mockExec.mock.calls[mockExec.mock.calls.length - 1];
|
|
328
|
+
expect(call[2]).toBe('Purchasely');
|
|
329
|
+
expect(call[3]).toBe('start');
|
|
330
|
+
expect(call[4]).toEqual([{ apiKey: 'API_KEY', sdkVersion: '5.6.2' }]);
|
|
331
|
+
call[0](true); // native success callback -> isConfigured
|
|
332
|
+
|
|
333
|
+
await expect(startPromise).resolves.toBe(true);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('rejects the Promise on native start failure', async () => {
|
|
337
|
+
const startPromise = Purchasely.builder('API_KEY').start();
|
|
338
|
+
|
|
339
|
+
const call = mockExec.mock.calls[mockExec.mock.calls.length - 1];
|
|
340
|
+
call[1]('Invalid API Key');
|
|
341
|
+
|
|
342
|
+
await expect(startPromise).rejects.toEqual({ message: 'Invalid API Key' });
|
|
343
|
+
});
|
|
231
344
|
});
|
|
232
345
|
|
|
233
|
-
describe('
|
|
346
|
+
describe('addEventListener (canonical, REC-18/PAR-18)', () => {
|
|
347
|
+
it('should call exec with correct parameters', () => {
|
|
348
|
+
const success = jest.fn();
|
|
349
|
+
const error = jest.fn();
|
|
350
|
+
|
|
351
|
+
Purchasely.addEventListener(success, error);
|
|
352
|
+
|
|
353
|
+
expect(mockExec).toHaveBeenCalledWith(success, error, 'Purchasely', 'addEventsListener', []);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('addEventsListener (deprecated alias)', () => {
|
|
234
358
|
it('should call exec with correct parameters', () => {
|
|
235
359
|
const success = jest.fn();
|
|
236
360
|
const error = jest.fn();
|
|
@@ -252,7 +376,21 @@ describe('Purchasely', () => {
|
|
|
252
376
|
});
|
|
253
377
|
});
|
|
254
378
|
|
|
255
|
-
describe('
|
|
379
|
+
describe('removeEventListener (canonical, REC-18/PAR-18)', () => {
|
|
380
|
+
it('should call exec with correct parameters', () => {
|
|
381
|
+
Purchasely.removeEventListener();
|
|
382
|
+
|
|
383
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
384
|
+
expect.any(Function),
|
|
385
|
+
expect.any(Function),
|
|
386
|
+
'Purchasely',
|
|
387
|
+
'removeEventsListener',
|
|
388
|
+
[]
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('removeEventsListener (deprecated alias)', () => {
|
|
256
394
|
it('should call exec with correct parameters', () => {
|
|
257
395
|
Purchasely.removeEventsListener();
|
|
258
396
|
|
|
@@ -291,6 +429,17 @@ describe('Purchasely', () => {
|
|
|
291
429
|
});
|
|
292
430
|
});
|
|
293
431
|
|
|
432
|
+
describe('isAnonymous (REC-12 / PAR-04)', () => {
|
|
433
|
+
it('should call exec with correct parameters', () => {
|
|
434
|
+
const success = jest.fn();
|
|
435
|
+
const error = jest.fn();
|
|
436
|
+
|
|
437
|
+
Purchasely.isAnonymous(success, error);
|
|
438
|
+
|
|
439
|
+
expect(mockExec).toHaveBeenCalledWith(success, error, 'Purchasely', 'isAnonymous', []);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
294
443
|
describe('userLogin', () => {
|
|
295
444
|
it('should call exec with correct parameters', () => {
|
|
296
445
|
const success = jest.fn();
|
|
@@ -308,7 +457,7 @@ describe('Purchasely', () => {
|
|
|
308
457
|
});
|
|
309
458
|
|
|
310
459
|
describe('userLogout', () => {
|
|
311
|
-
it('
|
|
460
|
+
it('defaults clearUserAttributes to true when omitted (PAR-30)', () => {
|
|
312
461
|
Purchasely.userLogout();
|
|
313
462
|
|
|
314
463
|
expect(mockExec).toHaveBeenCalledWith(
|
|
@@ -316,7 +465,19 @@ describe('Purchasely', () => {
|
|
|
316
465
|
expect.any(Function),
|
|
317
466
|
'Purchasely',
|
|
318
467
|
'userLogout',
|
|
319
|
-
[]
|
|
468
|
+
[true]
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('forwards an explicit clearUserAttributes value', () => {
|
|
473
|
+
Purchasely.userLogout(false);
|
|
474
|
+
|
|
475
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
476
|
+
expect.any(Function),
|
|
477
|
+
expect.any(Function),
|
|
478
|
+
'Purchasely',
|
|
479
|
+
'userLogout',
|
|
480
|
+
[false]
|
|
320
481
|
);
|
|
321
482
|
});
|
|
322
483
|
});
|
|
@@ -411,186 +572,425 @@ describe('Purchasely', () => {
|
|
|
411
572
|
});
|
|
412
573
|
});
|
|
413
574
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
575
|
+
// Purchasely 6.0: the v5 presentation surface (fetchPresentation*, presentPresentation*,
|
|
576
|
+
// backPresentation) is REMOVED, replaced by the promise-based v6 builder
|
|
577
|
+
// (Purchasely.presentation), which re-wraps the very same native exec actions.
|
|
578
|
+
describe('presentation builder (v6)', () => {
|
|
579
|
+
// Grabs the exec() call for a given native action, most recent first.
|
|
580
|
+
const execCallFor = (action) => {
|
|
581
|
+
const calls = mockExec.mock.calls.filter((c) => c[3] === action);
|
|
582
|
+
return calls[calls.length - 1];
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// normalizePresentation always returns this exact whitelist of documented fields
|
|
586
|
+
// (defaulting the ones the raw payload didn't carry to null); tests merge in the
|
|
587
|
+
// fields they care about rather than repeating the full shape each time.
|
|
588
|
+
const fullPresentation = (overrides) => ({
|
|
589
|
+
screenId: null,
|
|
590
|
+
placementId: null,
|
|
591
|
+
contentId: null,
|
|
592
|
+
audienceId: null,
|
|
593
|
+
abTestId: null,
|
|
594
|
+
abTestVariantId: null,
|
|
595
|
+
campaignId: null,
|
|
596
|
+
flowId: null,
|
|
597
|
+
language: null,
|
|
598
|
+
type: null,
|
|
599
|
+
plans: null,
|
|
600
|
+
metadata: null,
|
|
601
|
+
height: null,
|
|
602
|
+
...overrides,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
describe('sources', () => {
|
|
606
|
+
it('.placement(id).build().display() calls presentPresentationForPlacement', async () => {
|
|
607
|
+
const outcomePromise = Purchasely.presentation.placement('placement1').build().display();
|
|
608
|
+
|
|
609
|
+
const call = execCallFor('presentPresentationForPlacement');
|
|
610
|
+
expect(call[4]).toEqual(['placement1', null, undefined]);
|
|
611
|
+
call[0]({ purchaseResult: null, plan: null, closeReason: 'button', error: null, presentation: null });
|
|
612
|
+
|
|
613
|
+
const outcome = await outcomePromise;
|
|
614
|
+
expect(outcome.closeReason).toBe('button');
|
|
615
|
+
});
|
|
418
616
|
|
|
419
|
-
|
|
617
|
+
it('.screen(id).build().display() calls presentPresentationWithIdentifier', async () => {
|
|
618
|
+
const outcomePromise = Purchasely.presentation.screen('screen1').build().display();
|
|
420
619
|
|
|
421
|
-
|
|
422
|
-
expect.
|
|
423
|
-
error,
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
620
|
+
const call = execCallFor('presentPresentationWithIdentifier');
|
|
621
|
+
expect(call[4]).toEqual(['screen1', null, undefined]);
|
|
622
|
+
call[0]({ purchaseResult: null, plan: null, closeReason: 'programmatic', error: null, presentation: null });
|
|
623
|
+
|
|
624
|
+
await outcomePromise;
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('.defaultSource().build().display() calls presentPresentationForDefault', async () => {
|
|
628
|
+
const outcomePromise = Purchasely.presentation.defaultSource().build().display();
|
|
629
|
+
|
|
630
|
+
const call = execCallFor('presentPresentationForDefault');
|
|
631
|
+
expect(call[4]).toEqual([null, undefined]);
|
|
632
|
+
call[0]({ purchaseResult: null, plan: null, closeReason: 'button', error: null, presentation: null });
|
|
633
|
+
|
|
634
|
+
await outcomePromise;
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('.default() is an alias of .defaultSource()', async () => {
|
|
638
|
+
const outcomePromise = Purchasely.presentation.default().build().display();
|
|
639
|
+
|
|
640
|
+
const call = execCallFor('presentPresentationForDefault');
|
|
641
|
+
expect(call[4]).toEqual([null, undefined]);
|
|
642
|
+
call[0]({ purchaseResult: null, plan: null, closeReason: 'button', error: null, presentation: null });
|
|
643
|
+
|
|
644
|
+
await outcomePromise;
|
|
645
|
+
});
|
|
428
646
|
});
|
|
429
647
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
648
|
+
describe('contentId', () => {
|
|
649
|
+
it('is forwarded to the matching present* action', async () => {
|
|
650
|
+
const outcomePromise = Purchasely.presentation.placement('placement1').contentId('content1').build().display();
|
|
433
651
|
|
|
434
|
-
|
|
652
|
+
const call = execCallFor('presentPresentationForPlacement');
|
|
653
|
+
expect(call[4]).toEqual(['placement1', 'content1', undefined]);
|
|
654
|
+
call[0]({ closeReason: 'button' });
|
|
435
655
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
error,
|
|
439
|
-
'Purchasely',
|
|
440
|
-
'presentPresentationWithIdentifier',
|
|
441
|
-
['presentation1', 'content1', { type: 'fullScreen' }]
|
|
442
|
-
);
|
|
656
|
+
await outcomePromise;
|
|
657
|
+
});
|
|
443
658
|
});
|
|
444
659
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const transition = { type: 'drawer', dismissible: false, height: { type: 'percentage', value: 0.8 } };
|
|
660
|
+
describe('display(transition)', () => {
|
|
661
|
+
it('normalizes a display-mode string to a transition object', async () => {
|
|
662
|
+
const outcomePromise = Purchasely.presentation.screen('screen1').build().display(Purchasely.TransitionType.drawer);
|
|
449
663
|
|
|
450
|
-
|
|
664
|
+
const call = execCallFor('presentPresentationWithIdentifier');
|
|
665
|
+
expect(call[4][2]).toEqual({ type: 'drawer' });
|
|
666
|
+
call[0]({ closeReason: 'button' });
|
|
451
667
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
'
|
|
457
|
-
|
|
458
|
-
|
|
668
|
+
await outcomePromise;
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('passes a full transition object through unchanged', async () => {
|
|
672
|
+
const transition = { type: 'drawer', dismissible: false, height: { type: 'percentage', value: 0.8 } };
|
|
673
|
+
const outcomePromise = Purchasely.presentation.placement('placement1').build().display(transition);
|
|
674
|
+
|
|
675
|
+
const call = execCallFor('presentPresentationForPlacement');
|
|
676
|
+
expect(call[4][2]).toEqual(transition);
|
|
677
|
+
call[0]({ closeReason: 'button' });
|
|
678
|
+
|
|
679
|
+
await outcomePromise;
|
|
680
|
+
});
|
|
459
681
|
});
|
|
460
|
-
});
|
|
461
682
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
683
|
+
describe('backgroundColor', () => {
|
|
684
|
+
it('merges into the transition object on the direct present* path (no forced type)', async () => {
|
|
685
|
+
const outcomePromise = Purchasely.presentation
|
|
686
|
+
.placement('placement1')
|
|
687
|
+
.backgroundColor('#101010')
|
|
688
|
+
.build()
|
|
689
|
+
.display();
|
|
690
|
+
|
|
691
|
+
const call = execCallFor('presentPresentationForPlacement');
|
|
692
|
+
// No display-mode given: only backgroundColor, no `type`, so the backend
|
|
693
|
+
// transition default is still honored (CDV-W-12).
|
|
694
|
+
expect(call[4][2]).toEqual({ backgroundColor: '#101010' });
|
|
695
|
+
call[0]({ closeReason: 'button' });
|
|
696
|
+
await outcomePromise;
|
|
697
|
+
});
|
|
466
698
|
|
|
467
|
-
|
|
699
|
+
it('is forwarded as presentPresentation\'s native backgroundColor arg on the re-display path', async () => {
|
|
700
|
+
const request = Purchasely.presentation.screen('screen1').backgroundColor('#202020').build();
|
|
468
701
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
702
|
+
const preloadPromise = request.preload();
|
|
703
|
+
const rawFetched = { screenId: 'screen1', fetchId: 'ply_fetch_789' };
|
|
704
|
+
execCallFor('fetchPresentation')[0](rawFetched);
|
|
705
|
+
await preloadPromise;
|
|
706
|
+
|
|
707
|
+
const outcomePromise = request.display();
|
|
708
|
+
const displayCall = execCallFor('presentPresentation');
|
|
709
|
+
expect(displayCall[4][0]).toBe(rawFetched);
|
|
710
|
+
expect(displayCall[4][2]).toBe('#202020');
|
|
711
|
+
displayCall[0]({ closeReason: 'programmatic' });
|
|
712
|
+
await outcomePromise;
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('does not expose progressColor / displayCloseButton / displayBackButton (no Cordova native support)', () => {
|
|
716
|
+
const builder = Purchasely.presentation.placement('placement1');
|
|
717
|
+
expect(builder.progressColor).toBeUndefined();
|
|
718
|
+
expect(builder.displayCloseButton).toBeUndefined();
|
|
719
|
+
expect(builder.displayBackButton).toBeUndefined();
|
|
720
|
+
});
|
|
476
721
|
});
|
|
477
|
-
});
|
|
478
722
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
723
|
+
describe('lifecycle callbacks', () => {
|
|
724
|
+
it('routes presented/closeRequested envelopes to the builder callbacks, and the final outcome to onDismissed + the resolved promise', async () => {
|
|
725
|
+
const onPresented = jest.fn();
|
|
726
|
+
const onCloseRequested = jest.fn();
|
|
727
|
+
const onDismissed = jest.fn();
|
|
728
|
+
|
|
729
|
+
const outcomePromise = Purchasely.presentation
|
|
730
|
+
.placement('placement1')
|
|
731
|
+
.onPresented(onPresented)
|
|
732
|
+
.onCloseRequested(onCloseRequested)
|
|
733
|
+
.onDismissed(onDismissed)
|
|
734
|
+
.build()
|
|
735
|
+
.display();
|
|
736
|
+
|
|
737
|
+
const call = execCallFor('presentPresentationForPlacement');
|
|
738
|
+
const dispatch = call[0];
|
|
739
|
+
dispatch({ event: 'presented', presentation: { screenId: 's' } });
|
|
740
|
+
dispatch({ event: 'closeRequested' });
|
|
741
|
+
dispatch({ purchaseResult: 'cancelled', plan: null, closeReason: 'button', error: null, presentation: { screenId: 's' } });
|
|
742
|
+
|
|
743
|
+
const outcome = await outcomePromise;
|
|
744
|
+
|
|
745
|
+
// The 'presented' envelope's presentation is screenId-normalized like everywhere else.
|
|
746
|
+
expect(onPresented).toHaveBeenCalledWith(fullPresentation({ screenId: 's' }), null);
|
|
747
|
+
expect(onCloseRequested).toHaveBeenCalledTimes(1);
|
|
748
|
+
expect(onDismissed).toHaveBeenCalledWith(outcome);
|
|
749
|
+
expect(outcome).toEqual({
|
|
750
|
+
presentation: fullPresentation({ screenId: 's' }),
|
|
751
|
+
purchaseResult: 'cancelled',
|
|
752
|
+
plan: null,
|
|
753
|
+
closeReason: 'button',
|
|
754
|
+
error: null,
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
});
|
|
483
758
|
|
|
484
|
-
|
|
759
|
+
describe('outcome normalization (5 fields)', () => {
|
|
760
|
+
it('exposes exactly presentation/purchaseResult/plan/closeReason/error, with screenId tolerance', async () => {
|
|
761
|
+
const outcomePromise = Purchasely.presentation.placement('placement1').build().display();
|
|
762
|
+
|
|
763
|
+
const call = execCallFor('presentPresentationForPlacement');
|
|
764
|
+
call[0]({
|
|
765
|
+
result: 1, // legacy field: not part of the v6 builder's outcome contract
|
|
766
|
+
purchaseResult: 'purchased',
|
|
767
|
+
plan: { name: 'Plus' },
|
|
768
|
+
closeReason: null,
|
|
769
|
+
error: null,
|
|
770
|
+
presentation: { id: 'screen-id-only' }, // no screenId key: exercises the fallback
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const outcome = await outcomePromise;
|
|
774
|
+
expect(outcome).toEqual({
|
|
775
|
+
presentation: fullPresentation({ screenId: 'screen-id-only' }),
|
|
776
|
+
purchaseResult: 'purchased',
|
|
777
|
+
plan: { name: 'Plus' },
|
|
778
|
+
closeReason: null,
|
|
779
|
+
error: null,
|
|
780
|
+
});
|
|
781
|
+
expect(outcome.result).toBeUndefined();
|
|
782
|
+
});
|
|
485
783
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
'
|
|
490
|
-
'
|
|
491
|
-
|
|
492
|
-
|
|
784
|
+
it('synthesizes an outcome carrying `error` when the native call fails, without rejecting', async () => {
|
|
785
|
+
const outcomePromise = Purchasely.presentation.placement('placement1').build().display();
|
|
786
|
+
|
|
787
|
+
const call = execCallFor('presentPresentationForPlacement');
|
|
788
|
+
call[1]('Presentation not loaded'); // native error callback
|
|
789
|
+
|
|
790
|
+
const outcome = await outcomePromise;
|
|
791
|
+
expect(outcome.error).toBe('Presentation not loaded');
|
|
792
|
+
expect(outcome.presentation).toBeNull();
|
|
793
|
+
expect(outcome.closeReason).toBeNull();
|
|
794
|
+
});
|
|
493
795
|
});
|
|
494
|
-
});
|
|
495
796
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
797
|
+
describe('preload() -> display() re-display', () => {
|
|
798
|
+
it('preload() resolves a screenId-normalized presentation, and the follow-up display() re-displays it via presentPresentation carrying the native handle', async () => {
|
|
799
|
+
const request = Purchasely.presentation.placement('placement1').build();
|
|
800
|
+
|
|
801
|
+
const preloadPromise = request.preload();
|
|
802
|
+
const fetchCall = execCallFor('fetchPresentation');
|
|
803
|
+
expect(fetchCall[4]).toEqual(['placement1', null, null]);
|
|
804
|
+
|
|
805
|
+
const rawFetched = { screenId: 'onboarding-screen', placementId: 'placement1', fetchId: 'ply_fetch_123' };
|
|
806
|
+
fetchCall[0](rawFetched);
|
|
807
|
+
|
|
808
|
+
const presentation = await preloadPromise;
|
|
809
|
+
// The screenId-normalized data is present (methods are added on top, below).
|
|
810
|
+
expect(presentation).toMatchObject(fullPresentation({ screenId: 'onboarding-screen', placementId: 'placement1' }));
|
|
811
|
+
// The native re-display handle is never exposed on the returned object.
|
|
812
|
+
expect(presentation.fetchId).toBeUndefined();
|
|
813
|
+
expect(presentation.id).toBeUndefined();
|
|
814
|
+
|
|
815
|
+
const outcomePromise = request.display();
|
|
816
|
+
const displayCall = execCallFor('presentPresentation');
|
|
817
|
+
// The exact raw fetch payload (with its private handle) is forwarded as-is.
|
|
818
|
+
expect(displayCall[4][0]).toBe(rawFetched);
|
|
819
|
+
displayCall[0]({ closeReason: 'programmatic' });
|
|
501
820
|
|
|
502
|
-
|
|
821
|
+
const outcome = await outcomePromise;
|
|
822
|
+
expect(outcome.closeReason).toBe('programmatic');
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('the loaded presentation exposes display()/close()/back() delegating to its request', async () => {
|
|
826
|
+
const request = Purchasely.presentation.screen('screen1').build();
|
|
827
|
+
|
|
828
|
+
const preloadPromise = request.preload();
|
|
829
|
+
const rawFetched = { screenId: 'screen1', fetchId: 'ply_fetch_456' };
|
|
830
|
+
execCallFor('fetchPresentation')[0](rawFetched);
|
|
831
|
+
|
|
832
|
+
const loaded = await preloadPromise;
|
|
833
|
+
expect(typeof loaded.display).toBe('function');
|
|
834
|
+
expect(typeof loaded.close).toBe('function');
|
|
835
|
+
expect(typeof loaded.back).toBe('function');
|
|
836
|
+
|
|
837
|
+
// display() on the loaded object re-displays via the same request/handle.
|
|
838
|
+
const outcomePromise = loaded.display();
|
|
839
|
+
const displayCall = execCallFor('presentPresentation');
|
|
840
|
+
expect(displayCall[4][0]).toBe(rawFetched);
|
|
841
|
+
displayCall[0]({ closeReason: 'programmatic' });
|
|
842
|
+
await outcomePromise;
|
|
843
|
+
|
|
844
|
+
// close() / back() delegate to the request's native actions.
|
|
845
|
+
loaded.close();
|
|
846
|
+
expect(execCallFor('closeAllScreens')).toBeDefined();
|
|
847
|
+
loaded.back();
|
|
848
|
+
expect(execCallFor('backPresentation')).toBeDefined();
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('rejects preload() on native failure', async () => {
|
|
852
|
+
const request = Purchasely.presentation.screen('screen1').build();
|
|
853
|
+
const preloadPromise = request.preload();
|
|
503
854
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
dispatch({ event: 'closeRequested' });
|
|
507
|
-
dispatch({ result: 1, purchaseResult: 'cancelled', closeReason: 'button' });
|
|
855
|
+
const fetchCall = execCallFor('fetchPresentation');
|
|
856
|
+
fetchCall[1]('Screen not found');
|
|
508
857
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
858
|
+
await expect(preloadPromise).rejects.toEqual({ message: 'Screen not found' });
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// Android asymmetry regression (v6 audit): onPresented/onCloseRequested previously only
|
|
862
|
+
// fired on the direct present* path. The re-display action (presentPresentation, reached
|
|
863
|
+
// via preload() -> display()) must route the very same keep-alive envelopes to the
|
|
864
|
+
// builder's callbacks, resolving the display() Promise only once the final (non-kept)
|
|
865
|
+
// outcome envelope arrives -- envelopes strictly before resolution.
|
|
866
|
+
it('routes presented/closeRequested envelopes on the preload() -> display() path too, resolving only at the final outcome', async () => {
|
|
867
|
+
const onPresented = jest.fn();
|
|
868
|
+
const onCloseRequested = jest.fn();
|
|
869
|
+
const order = [];
|
|
870
|
+
onPresented.mockImplementation(() => order.push('presented'));
|
|
871
|
+
onCloseRequested.mockImplementation(() => order.push('closeRequested'));
|
|
872
|
+
|
|
873
|
+
const request = Purchasely.presentation
|
|
874
|
+
.screen('screen1')
|
|
875
|
+
.onPresented(onPresented)
|
|
876
|
+
.onCloseRequested(onCloseRequested)
|
|
877
|
+
.build();
|
|
878
|
+
|
|
879
|
+
const preloadPromise = request.preload();
|
|
880
|
+
const rawFetched = { screenId: 'screen1', fetchId: 'ply_fetch_789' };
|
|
881
|
+
execCallFor('fetchPresentation')[0](rawFetched);
|
|
882
|
+
await preloadPromise;
|
|
883
|
+
|
|
884
|
+
const outcomePromise = request.display();
|
|
885
|
+
const displayCall = execCallFor('presentPresentation');
|
|
886
|
+
const dispatch = displayCall[0];
|
|
887
|
+
|
|
888
|
+
dispatch({ event: 'presented', presentation: { screenId: 'screen1' } });
|
|
889
|
+
dispatch({ event: 'closeRequested' });
|
|
890
|
+
expect(order).toEqual(['presented', 'closeRequested']);
|
|
891
|
+
// Neither envelope resolves the Promise -- only the final outcome does.
|
|
892
|
+
let settled = false;
|
|
893
|
+
outcomePromise.then(() => { settled = true; });
|
|
894
|
+
await Promise.resolve();
|
|
895
|
+
expect(settled).toBe(false);
|
|
896
|
+
order.push('outcome-dispatched');
|
|
897
|
+
|
|
898
|
+
dispatch({ closeReason: 'button', presentation: { screenId: 'screen1' } });
|
|
899
|
+
const outcome = await outcomePromise;
|
|
900
|
+
|
|
901
|
+
// Envelopes fired strictly before the Promise resolved.
|
|
902
|
+
expect(order).toEqual(['presented', 'closeRequested', 'outcome-dispatched']);
|
|
903
|
+
expect(onPresented).toHaveBeenCalledWith(fullPresentation({ screenId: 'screen1' }), null);
|
|
904
|
+
expect(onCloseRequested).toHaveBeenCalledTimes(1);
|
|
905
|
+
expect(outcome.closeReason).toBe('button');
|
|
906
|
+
});
|
|
512
907
|
});
|
|
513
|
-
});
|
|
514
908
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
909
|
+
describe('onLoaded builder callback', () => {
|
|
910
|
+
it('fires onLoaded with the screenId-normalized presentation once preload() loads it', async () => {
|
|
911
|
+
const onLoaded = jest.fn();
|
|
912
|
+
const request = Purchasely.presentation.screen('screen1').onLoaded(onLoaded).build();
|
|
518
913
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
914
|
+
const preloadPromise = request.preload();
|
|
915
|
+
const rawFetched = { screenId: 'screen1', fetchId: 'ply_fetch_loaded' };
|
|
916
|
+
execCallFor('fetchPresentation')[0](rawFetched);
|
|
917
|
+
|
|
918
|
+
await preloadPromise;
|
|
919
|
+
|
|
920
|
+
expect(onLoaded).toHaveBeenCalledWith(fullPresentation({ screenId: 'screen1' }), null);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('does not fire onLoaded when preload() fails (parity with RN/Flutter)', async () => {
|
|
924
|
+
const onLoaded = jest.fn();
|
|
925
|
+
const request = Purchasely.presentation.screen('screen1').onLoaded(onLoaded).build();
|
|
926
|
+
|
|
927
|
+
const preloadPromise = request.preload();
|
|
928
|
+
execCallFor('fetchPresentation')[1]('Screen not found');
|
|
929
|
+
|
|
930
|
+
await expect(preloadPromise).rejects.toEqual({ message: 'Screen not found' });
|
|
931
|
+
expect(onLoaded).not.toHaveBeenCalled();
|
|
932
|
+
});
|
|
526
933
|
});
|
|
527
|
-
});
|
|
528
934
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
935
|
+
describe('display() direct (no prior preload())', () => {
|
|
936
|
+
it('calls the matching present* action directly, without ever calling fetchPresentation', async () => {
|
|
937
|
+
mockExec.mockClear();
|
|
938
|
+
const outcomePromise = Purchasely.presentation.screen('screen1').build().display();
|
|
533
939
|
|
|
534
|
-
|
|
940
|
+
expect(mockExec.mock.calls.some((c) => c[3] === 'fetchPresentation')).toBe(false);
|
|
941
|
+
expect(mockExec.mock.calls.some((c) => c[3] === 'presentPresentationWithIdentifier')).toBe(true);
|
|
535
942
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
'fetchPresentation',
|
|
541
|
-
[null, 'presentation1', 'content1']
|
|
542
|
-
);
|
|
943
|
+
const call = execCallFor('presentPresentationWithIdentifier');
|
|
944
|
+
call[0]({ closeReason: 'button' });
|
|
945
|
+
await outcomePromise;
|
|
946
|
+
});
|
|
543
947
|
});
|
|
544
|
-
});
|
|
545
948
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
949
|
+
describe('request.close()', () => {
|
|
950
|
+
it('delegates to closeAllScreens (current bridge semantics)', () => {
|
|
951
|
+
mockExec.mockClear();
|
|
952
|
+
const request = Purchasely.presentation.placement('placement1').build();
|
|
550
953
|
|
|
551
|
-
|
|
954
|
+
request.close();
|
|
552
955
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
956
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
957
|
+
expect.any(Function),
|
|
958
|
+
expect.any(Function),
|
|
959
|
+
'Purchasely',
|
|
960
|
+
'closeAllScreens',
|
|
961
|
+
[]
|
|
962
|
+
);
|
|
963
|
+
});
|
|
560
964
|
});
|
|
561
|
-
});
|
|
562
965
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
966
|
+
describe('request.back()', () => {
|
|
967
|
+
it('calls exec with the backPresentation native action', () => {
|
|
968
|
+
mockExec.mockClear();
|
|
969
|
+
const request = Purchasely.presentation.placement('placement1').build();
|
|
567
970
|
|
|
568
|
-
|
|
971
|
+
request.back();
|
|
569
972
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
973
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
974
|
+
expect.any(Function),
|
|
975
|
+
expect.any(Function),
|
|
976
|
+
'Purchasely',
|
|
977
|
+
'backPresentation',
|
|
978
|
+
[]
|
|
979
|
+
);
|
|
980
|
+
});
|
|
577
981
|
});
|
|
578
982
|
});
|
|
579
983
|
|
|
580
|
-
describe('
|
|
581
|
-
it('should call exec with
|
|
582
|
-
|
|
583
|
-
const error = jest.fn();
|
|
584
|
-
const presentation = { id: 'test' };
|
|
585
|
-
|
|
586
|
-
Purchasely.presentPresentation(presentation, Purchasely.TransitionType.fullScreen, '#FFFFFF', success, error);
|
|
984
|
+
describe('removeDefaultPresentationDismissHandler', () => {
|
|
985
|
+
it('should call exec with correct parameters', () => {
|
|
986
|
+
Purchasely.removeDefaultPresentationDismissHandler();
|
|
587
987
|
|
|
588
988
|
expect(mockExec).toHaveBeenCalledWith(
|
|
589
989
|
expect.any(Function),
|
|
590
|
-
|
|
990
|
+
expect.any(Function),
|
|
591
991
|
'Purchasely',
|
|
592
|
-
'
|
|
593
|
-
[
|
|
992
|
+
'removeDefaultPresentationDismissHandler',
|
|
993
|
+
[]
|
|
594
994
|
);
|
|
595
995
|
});
|
|
596
996
|
});
|
|
@@ -760,6 +1160,37 @@ describe('Purchasely', () => {
|
|
|
760
1160
|
);
|
|
761
1161
|
});
|
|
762
1162
|
|
|
1163
|
+
// Commitment fields (iOS 26.4+, Apple only) ride on the plan carried by the purchase
|
|
1164
|
+
// payload's `parameters.plan`. The JS layer forwards `parameters` verbatim, so a plan's
|
|
1165
|
+
// `commitmentInfo` must survive untouched — this guards against a future param whitelist
|
|
1166
|
+
// silently dropping it.
|
|
1167
|
+
it('forwards a purchase payload plan carrying commitmentInfo to the handler', async () => {
|
|
1168
|
+
const handler = jest.fn().mockReturnValue(Purchasely.InterceptResult.success);
|
|
1169
|
+
Purchasely.interceptAction('purchase', handler);
|
|
1170
|
+
const nativeSuccess = nativeInterceptorFor('purchase');
|
|
1171
|
+
|
|
1172
|
+
const parameters = {
|
|
1173
|
+
plan: {
|
|
1174
|
+
vendorId: 'plan_monthly_12m',
|
|
1175
|
+
commitmentInfo: [
|
|
1176
|
+
{
|
|
1177
|
+
billingPlanType: Purchasely.BillingPlanType.monthly,
|
|
1178
|
+
billingPrice: 9.99,
|
|
1179
|
+
billingPeriod: 'P1M',
|
|
1180
|
+
totalPrice: 119.88,
|
|
1181
|
+
totalPeriod: 'P1Y',
|
|
1182
|
+
totalDuration: 12,
|
|
1183
|
+
},
|
|
1184
|
+
],
|
|
1185
|
+
},
|
|
1186
|
+
};
|
|
1187
|
+
nativeSuccess({ action: 'purchase', callbackId: 'purchase#c', info: null, parameters });
|
|
1188
|
+
await flush();
|
|
1189
|
+
|
|
1190
|
+
expect(handler).toHaveBeenCalledWith(null, parameters);
|
|
1191
|
+
expect(handler.mock.calls[0][1].plan.commitmentInfo[0].billingPlanType).toBe(2);
|
|
1192
|
+
});
|
|
1193
|
+
|
|
763
1194
|
it('ignores events whose action does not match the registered kind', async () => {
|
|
764
1195
|
const handler = jest.fn();
|
|
765
1196
|
Purchasely.interceptAction('restore', handler);
|
|
@@ -833,7 +1264,7 @@ describe('Purchasely', () => {
|
|
|
833
1264
|
});
|
|
834
1265
|
|
|
835
1266
|
describe('userSubscriptions', () => {
|
|
836
|
-
it('
|
|
1267
|
+
it('defaults invalidateCache to false when omitted (PAR-29)', () => {
|
|
837
1268
|
const success = jest.fn();
|
|
838
1269
|
const error = jest.fn();
|
|
839
1270
|
|
|
@@ -844,13 +1275,28 @@ describe('Purchasely', () => {
|
|
|
844
1275
|
expect.any(Function),
|
|
845
1276
|
'Purchasely',
|
|
846
1277
|
'userSubscriptions',
|
|
847
|
-
[]
|
|
1278
|
+
[false]
|
|
1279
|
+
);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
it('forwards an explicit invalidateCache value', () => {
|
|
1283
|
+
const success = jest.fn();
|
|
1284
|
+
const error = jest.fn();
|
|
1285
|
+
|
|
1286
|
+
Purchasely.userSubscriptions(success, error, true);
|
|
1287
|
+
|
|
1288
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
1289
|
+
success,
|
|
1290
|
+
expect.any(Function),
|
|
1291
|
+
'Purchasely',
|
|
1292
|
+
'userSubscriptions',
|
|
1293
|
+
[true]
|
|
848
1294
|
);
|
|
849
1295
|
});
|
|
850
1296
|
});
|
|
851
1297
|
|
|
852
1298
|
describe('userSubscriptionsHistory', () => {
|
|
853
|
-
it('
|
|
1299
|
+
it('defaults invalidateCache to false when omitted (PAR-29)', () => {
|
|
854
1300
|
const success = jest.fn();
|
|
855
1301
|
const error = jest.fn();
|
|
856
1302
|
|
|
@@ -861,7 +1307,22 @@ describe('Purchasely', () => {
|
|
|
861
1307
|
expect.any(Function),
|
|
862
1308
|
'Purchasely',
|
|
863
1309
|
'userSubscriptionsHistory',
|
|
864
|
-
[]
|
|
1310
|
+
[false]
|
|
1311
|
+
);
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it('forwards an explicit invalidateCache value', () => {
|
|
1315
|
+
const success = jest.fn();
|
|
1316
|
+
const error = jest.fn();
|
|
1317
|
+
|
|
1318
|
+
Purchasely.userSubscriptionsHistory(success, error, true);
|
|
1319
|
+
|
|
1320
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
1321
|
+
success,
|
|
1322
|
+
expect.any(Function),
|
|
1323
|
+
'Purchasely',
|
|
1324
|
+
'userSubscriptionsHistory',
|
|
1325
|
+
[true]
|
|
865
1326
|
);
|
|
866
1327
|
});
|
|
867
1328
|
});
|
|
@@ -880,29 +1341,38 @@ describe('Purchasely', () => {
|
|
|
880
1341
|
});
|
|
881
1342
|
});
|
|
882
1343
|
|
|
883
|
-
describe('
|
|
1344
|
+
describe('closeAllScreens', () => {
|
|
884
1345
|
it('should call exec with correct parameters', () => {
|
|
885
|
-
|
|
1346
|
+
const success = jest.fn();
|
|
1347
|
+
const error = jest.fn();
|
|
1348
|
+
|
|
1349
|
+
Purchasely.closeAllScreens(success, error);
|
|
1350
|
+
|
|
1351
|
+
expect(mockExec).toHaveBeenCalledWith(success, error, 'Purchasely', 'closeAllScreens', []);
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
it('should default callbacks when none provided', () => {
|
|
1355
|
+
Purchasely.closeAllScreens();
|
|
886
1356
|
|
|
887
1357
|
expect(mockExec).toHaveBeenCalledWith(
|
|
888
1358
|
expect.any(Function),
|
|
889
1359
|
expect.any(Function),
|
|
890
1360
|
'Purchasely',
|
|
891
|
-
'
|
|
1361
|
+
'closeAllScreens',
|
|
892
1362
|
[]
|
|
893
1363
|
);
|
|
894
1364
|
});
|
|
895
1365
|
});
|
|
896
1366
|
|
|
897
|
-
describe('
|
|
898
|
-
it('should
|
|
899
|
-
Purchasely.
|
|
1367
|
+
describe('closePresentation (deprecated alias of closeAllScreens)', () => {
|
|
1368
|
+
it('should delegate to closeAllScreens', () => {
|
|
1369
|
+
Purchasely.closePresentation();
|
|
900
1370
|
|
|
901
1371
|
expect(mockExec).toHaveBeenCalledWith(
|
|
902
1372
|
expect.any(Function),
|
|
903
1373
|
expect.any(Function),
|
|
904
1374
|
'Purchasely',
|
|
905
|
-
'
|
|
1375
|
+
'closeAllScreens',
|
|
906
1376
|
[]
|
|
907
1377
|
);
|
|
908
1378
|
});
|
|
@@ -1087,6 +1557,79 @@ describe('Purchasely', () => {
|
|
|
1087
1557
|
);
|
|
1088
1558
|
});
|
|
1089
1559
|
});
|
|
1560
|
+
|
|
1561
|
+
describe('getBuiltInAttributes (PAR-07)', () => {
|
|
1562
|
+
it('should call exec with correct parameters', () => {
|
|
1563
|
+
const success = jest.fn();
|
|
1564
|
+
const error = jest.fn();
|
|
1565
|
+
|
|
1566
|
+
Purchasely.getBuiltInAttributes(success, error);
|
|
1567
|
+
|
|
1568
|
+
expect(mockExec).toHaveBeenCalledWith(success, error, 'Purchasely', 'getBuiltInAttributes', []);
|
|
1569
|
+
});
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
describe('getBuiltInAttribute (PAR-07)', () => {
|
|
1573
|
+
it('should call exec with correct parameters', () => {
|
|
1574
|
+
const success = jest.fn();
|
|
1575
|
+
const error = jest.fn();
|
|
1576
|
+
|
|
1577
|
+
Purchasely.getBuiltInAttribute('ply_session_count', success, error);
|
|
1578
|
+
|
|
1579
|
+
expect(mockExec).toHaveBeenCalledWith(success, error, 'Purchasely', 'getBuiltInAttribute', ['ply_session_count']);
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
describe('userAttributes (REC-12 / PAR-03)', () => {
|
|
1584
|
+
it('should call exec with correct parameters', () => {
|
|
1585
|
+
const success = jest.fn();
|
|
1586
|
+
const error = jest.fn();
|
|
1587
|
+
|
|
1588
|
+
Purchasely.userAttributes(success, error);
|
|
1589
|
+
|
|
1590
|
+
expect(mockExec).toHaveBeenCalledWith(success, error, 'Purchasely', 'userAttributes', []);
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
describe('incrementUserAttribute (REC-12 / PAR-02)', () => {
|
|
1595
|
+
it('should call exec with correct parameters', () => {
|
|
1596
|
+
Purchasely.incrementUserAttribute('counter', 5);
|
|
1597
|
+
|
|
1598
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
1599
|
+
expect.any(Function),
|
|
1600
|
+
expect.any(Function),
|
|
1601
|
+
'Purchasely',
|
|
1602
|
+
'incrementUserAttribute',
|
|
1603
|
+
['counter', 5]
|
|
1604
|
+
);
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
it('should forward an omitted value as-is (native defaults it to 1)', () => {
|
|
1608
|
+
Purchasely.incrementUserAttribute('counter');
|
|
1609
|
+
|
|
1610
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
1611
|
+
expect.any(Function),
|
|
1612
|
+
expect.any(Function),
|
|
1613
|
+
'Purchasely',
|
|
1614
|
+
'incrementUserAttribute',
|
|
1615
|
+
['counter', undefined]
|
|
1616
|
+
);
|
|
1617
|
+
});
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
describe('decrementUserAttribute (REC-12 / PAR-02)', () => {
|
|
1621
|
+
it('should call exec with correct parameters', () => {
|
|
1622
|
+
Purchasely.decrementUserAttribute('counter', 3);
|
|
1623
|
+
|
|
1624
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
1625
|
+
expect.any(Function),
|
|
1626
|
+
expect.any(Function),
|
|
1627
|
+
'Purchasely',
|
|
1628
|
+
'decrementUserAttribute',
|
|
1629
|
+
['counter', 3]
|
|
1630
|
+
);
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1090
1633
|
});
|
|
1091
1634
|
|
|
1092
1635
|
describe('isEligibleForIntroOffer', () => {
|
|
@@ -1158,4 +1701,126 @@ describe('Purchasely', () => {
|
|
|
1158
1701
|
);
|
|
1159
1702
|
});
|
|
1160
1703
|
});
|
|
1704
|
+
|
|
1705
|
+
describe('Dynamic Offerings (PAR-05)', () => {
|
|
1706
|
+
describe('setDynamicOffering', () => {
|
|
1707
|
+
it('should call exec with correct parameters, forwarding billingPlanType', () => {
|
|
1708
|
+
const success = jest.fn();
|
|
1709
|
+
const error = jest.fn();
|
|
1710
|
+
|
|
1711
|
+
Purchasely.setDynamicOffering('ref_1', 'plan_vendor_id', 'offer_vendor_id', Purchasely.BillingPlanType.monthly, success, error);
|
|
1712
|
+
|
|
1713
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
1714
|
+
success,
|
|
1715
|
+
error,
|
|
1716
|
+
'Purchasely',
|
|
1717
|
+
'setDynamicOffering',
|
|
1718
|
+
['ref_1', 'plan_vendor_id', 'offer_vendor_id', 2]
|
|
1719
|
+
);
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
it('should forward a null offerVendorId and default billingPlanType to unspecified when omitted', () => {
|
|
1723
|
+
Purchasely.setDynamicOffering('ref_1', 'plan_vendor_id', null, undefined, jest.fn(), jest.fn());
|
|
1724
|
+
|
|
1725
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
1726
|
+
expect.any(Function),
|
|
1727
|
+
expect.any(Function),
|
|
1728
|
+
'Purchasely',
|
|
1729
|
+
'setDynamicOffering',
|
|
1730
|
+
['ref_1', 'plan_vendor_id', null, 0]
|
|
1731
|
+
);
|
|
1732
|
+
});
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
describe('getDynamicOfferings', () => {
|
|
1736
|
+
it('should call exec with correct parameters', () => {
|
|
1737
|
+
const success = jest.fn();
|
|
1738
|
+
const error = jest.fn();
|
|
1739
|
+
|
|
1740
|
+
Purchasely.getDynamicOfferings(success, error);
|
|
1741
|
+
|
|
1742
|
+
expect(mockExec).toHaveBeenCalledWith(success, error, 'Purchasely', 'getDynamicOfferings', []);
|
|
1743
|
+
});
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
describe('removeDynamicOffering', () => {
|
|
1747
|
+
it('should call exec with correct parameters', () => {
|
|
1748
|
+
Purchasely.removeDynamicOffering('ref_1');
|
|
1749
|
+
|
|
1750
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
1751
|
+
expect.any(Function),
|
|
1752
|
+
expect.any(Function),
|
|
1753
|
+
'Purchasely',
|
|
1754
|
+
'removeDynamicOffering',
|
|
1755
|
+
['ref_1']
|
|
1756
|
+
);
|
|
1757
|
+
});
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
describe('clearDynamicOfferings', () => {
|
|
1761
|
+
it('should call exec with correct parameters', () => {
|
|
1762
|
+
Purchasely.clearDynamicOfferings();
|
|
1763
|
+
|
|
1764
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
1765
|
+
expect.any(Function),
|
|
1766
|
+
expect.any(Function),
|
|
1767
|
+
'Purchasely',
|
|
1768
|
+
'clearDynamicOfferings',
|
|
1769
|
+
[]
|
|
1770
|
+
);
|
|
1771
|
+
});
|
|
1772
|
+
});
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
// Purchasely 6.0: these v5 APIs were deliberately removed (see MIGRATION-v6.md).
|
|
1776
|
+
// Asserting their absence guards against accidental resurrection/typos reintroducing
|
|
1777
|
+
// a v5-shaped surface alongside the v6 replacements.
|
|
1778
|
+
describe('Removed v5 APIs (should not exist in v6)', () => {
|
|
1779
|
+
it('removes the presentation methods with no v6 native screen equivalent', () => {
|
|
1780
|
+
expect(Purchasely.presentSubscriptions).toBeUndefined();
|
|
1781
|
+
expect(Purchasely.presentProductWithIdentifier).toBeUndefined();
|
|
1782
|
+
expect(Purchasely.presentPlanWithIdentifier).toBeUndefined();
|
|
1783
|
+
expect(Purchasely.showPresentation).toBeUndefined();
|
|
1784
|
+
expect(Purchasely.hidePresentation).toBeUndefined();
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
// The imperative presentation surface is replaced by the Purchasely.presentation
|
|
1788
|
+
// builder (see the 'presentation builder (v6)' suite above).
|
|
1789
|
+
it('removes the imperative presentation surface in favor of Purchasely.presentation', () => {
|
|
1790
|
+
expect(Purchasely.fetchPresentation).toBeUndefined();
|
|
1791
|
+
expect(Purchasely.fetchPresentationForPlacement).toBeUndefined();
|
|
1792
|
+
expect(Purchasely.fetchPresentationForDefault).toBeUndefined();
|
|
1793
|
+
expect(Purchasely.presentPresentationWithIdentifier).toBeUndefined();
|
|
1794
|
+
expect(Purchasely.presentPresentationForPlacement).toBeUndefined();
|
|
1795
|
+
expect(Purchasely.presentPresentationForDefault).toBeUndefined();
|
|
1796
|
+
expect(Purchasely.presentPresentation).toBeUndefined();
|
|
1797
|
+
expect(Purchasely.backPresentation).toBeUndefined();
|
|
1798
|
+
|
|
1799
|
+
expect(typeof Purchasely.presentation.placement).toBe('function');
|
|
1800
|
+
expect(typeof Purchasely.presentation.screen).toBe('function');
|
|
1801
|
+
expect(typeof Purchasely.presentation.defaultSource).toBe('function');
|
|
1802
|
+
expect(typeof Purchasely.presentation.default).toBe('function');
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
it('removes the single global action interceptor in favor of interceptAction(kind, handler)', () => {
|
|
1806
|
+
expect(Purchasely.setPaywallActionInterceptor).toBeUndefined();
|
|
1807
|
+
expect(Purchasely.onProcessAction).toBeUndefined();
|
|
1808
|
+
expect(Purchasely.PaywallAction).toBeUndefined();
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
it('removes the renamed deeplink methods', () => {
|
|
1812
|
+
expect(Purchasely.readyToOpenDeeplink).toBeUndefined();
|
|
1813
|
+
expect(Purchasely.isDeeplinkHandled).toBeUndefined();
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
it('removes the renamed default dismiss handler', () => {
|
|
1817
|
+
expect(Purchasely.setDefaultPresentationResultHandler).toBeUndefined();
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
it('removes the RunningMode values dropped by native 6.0', () => {
|
|
1821
|
+
expect(Purchasely.RunningMode.paywallObserver).toBeUndefined();
|
|
1822
|
+
expect(Purchasely.RunningMode.transactionOnly).toBeUndefined();
|
|
1823
|
+
expect(Object.keys(Purchasely.RunningMode).sort()).toEqual(['full', 'observer']);
|
|
1824
|
+
});
|
|
1825
|
+
});
|
|
1161
1826
|
});
|