@purchasely/cordova-plugin-purchasely 6.0.0-rc.3 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +77 -0
- package/README.md +22 -14
- package/package.json +11 -2
- package/plugin.xml +4 -4
- 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 +180 -10
- package/src/ios/Hybrid/PLYPlan+Hybrid.m +19 -0
- package/src/ios/Hybrid/PLYSubscription+Hybrid.m +13 -0
- package/www/Purchasely.js +390 -46
- package/__tests__/Purchasely.test.js +0 -1161
- package/example/android.sh +0 -10
- package/example/config.xml +0 -27
- package/example/e2e/README.md +0 -44
- package/example/e2e/helpers/driver.js +0 -70
- package/example/e2e/package.json +0 -19
- package/example/e2e/specs/bridge.e2e.js +0 -51
- package/example/e2e/specs/dismiss.e2e.js +0 -37
- package/example/e2e/tools/ci_run_e2e.sh +0 -49
- package/example/e2e/tools/ci_run_e2e_ios.sh +0 -46
- package/example/e2e/wdio.android.conf.js +0 -21
- package/example/e2e/wdio.ios.conf.js +0 -19
- package/example/e2e/wdio.shared.conf.js +0 -28
- package/example/ios.sh +0 -16
- package/example/package-lock.json +0 -859
- package/example/package.json +0 -37
- package/example/www/css/index.css +0 -110
- package/example/www/img/logo.png +0 -0
- package/example/www/index.html +0 -78
- package/example/www/js/index.js +0 -449
- package/purchasely.iml +0 -11
|
@@ -72,9 +72,18 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
72
72
|
|
|
73
73
|
// Presentation currently displayed (used for back()/close()).
|
|
74
74
|
private var displayedPresentation: PLYPresentationBase.Loaded? = null
|
|
75
|
-
// Presentations preloaded by fetchPresentation, keyed by a synthetic handle
|
|
76
|
-
//
|
|
75
|
+
// Presentations preloaded by fetchPresentation, keyed by a synthetic handle sent back to
|
|
76
|
+
// JS under the private `fetchId` key (NOT `id` -- `screenId` is the authoritative public
|
|
77
|
+
// presentation identifier; `fetchId` is an internal re-display lookup key, never documented
|
|
78
|
+
// as public API) so presentPresentation can re-display them.
|
|
77
79
|
private val loadedPresentations = ConcurrentHashMap<String, PLYPresentationBase.Loaded>()
|
|
80
|
+
// v6: the native SDK fixes onPresented/onCloseRequested at builder.build() time (before
|
|
81
|
+
// preload()) -- there is no display invocation, hence no CallbackContext, yet at fetch
|
|
82
|
+
// time. Seeded closures in fetchPresentation() capture the handle and resolve the live
|
|
83
|
+
// CallbackContext from here lazily, at fire time; presentPresentation() registers the
|
|
84
|
+
// invocation that's actually re-displaying under the same handle just before calling
|
|
85
|
+
// display(). Keyed identically to loadedPresentations.
|
|
86
|
+
private val lifecycleCallbacks = ConcurrentHashMap<String, CallbackContext>()
|
|
78
87
|
|
|
79
88
|
override fun onDestroy() {
|
|
80
89
|
job.cancel()
|
|
@@ -90,14 +99,14 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
90
99
|
when (action) {
|
|
91
100
|
"start" -> start(args.getJSONObject(0), callbackContext)
|
|
92
101
|
|
|
93
|
-
"close" -> close()
|
|
94
102
|
"addEventsListener" -> addEventsListener(callbackContext)
|
|
95
103
|
"addUserAttributeListener" -> addUserAttributesListener(callbackContext)
|
|
96
104
|
"removeUserAttributeListener" -> removeUserAttributesListener()
|
|
97
105
|
"removeEventsListener" -> removeEventsListener()
|
|
98
106
|
"getAnonymousUserId" -> getAnonymousUserId(callbackContext)
|
|
107
|
+
"isAnonymous" -> isAnonymous(callbackContext)
|
|
99
108
|
"userLogin" -> userLogin(getStringFromJson(args.getString(0)), callbackContext)
|
|
100
|
-
"userLogout" -> userLogout()
|
|
109
|
+
"userLogout" -> userLogout(args.optBoolean(0, true))
|
|
101
110
|
"setLanguage" -> setLanguage(getStringFromJson(args.getString(0)))
|
|
102
111
|
"setLogLevel" -> setLogLevel(args.getInt(0))
|
|
103
112
|
"setThemeMode" -> setThemeMode(args.getInt(0))
|
|
@@ -145,8 +154,8 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
145
154
|
)
|
|
146
155
|
"restoreAllProducts" -> restoreAllProducts(callbackContext)
|
|
147
156
|
"silentRestoreAllProducts" -> restoreAllProducts(callbackContext)
|
|
148
|
-
"userSubscriptions" -> userSubscriptions(callbackContext)
|
|
149
|
-
"userSubscriptionsHistory" -> userSubscriptionsHistory(callbackContext)
|
|
157
|
+
"userSubscriptions" -> userSubscriptions(args.optBoolean(0, false), callbackContext)
|
|
158
|
+
"userSubscriptionsHistory" -> userSubscriptionsHistory(args.optBoolean(0, false), callbackContext)
|
|
150
159
|
"handleDeeplink" -> handleDeeplink(
|
|
151
160
|
getStringFromJson(args.getString(0)),
|
|
152
161
|
callbackContext
|
|
@@ -173,6 +182,7 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
173
182
|
"unregisterActionInterceptor" -> unregisterActionInterceptor(getStringFromJson(args.getString(0)))
|
|
174
183
|
"completeActionInterceptor" -> completeActionInterceptor(getStringFromJson(args.getString(0)), getStringFromJson(args.getString(1)))
|
|
175
184
|
"closePresentation" -> closePresentation(callbackContext)
|
|
185
|
+
"closeAllScreens" -> closePresentation(callbackContext)
|
|
176
186
|
"backPresentation" -> backPresentation(callbackContext)
|
|
177
187
|
"userDidConsumeSubscriptionContent" -> userDidConsumeSubscriptionContent()
|
|
178
188
|
"setUserAttributeWithString" -> setUserAttributeWithString(getStringFromJson(args.getString(0)), getStringFromJson(args.getString(1)), getStringFromJson(args.optString(2)))
|
|
@@ -185,13 +195,27 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
185
195
|
"setUserAttributeWithDoubleArray" -> setUserAttributeWithDoubleArray(getStringFromJson(args.getString(0)), args.getJSONArray(1), getStringFromJson(args.optString(2)))
|
|
186
196
|
"setUserAttributeWithBooleanArray" -> setUserAttributeWithBooleanArray(getStringFromJson(args.getString(0)), args.getJSONArray(1), getStringFromJson(args.optString(2)))
|
|
187
197
|
"userAttribute" -> userAttribute(getStringFromJson(args.getString(0)), callbackContext)
|
|
198
|
+
"userAttributes" -> userAttributes(callbackContext)
|
|
199
|
+
"incrementUserAttribute" -> incrementUserAttribute(getStringFromJson(args.getString(0)), args.optInt(1, 1))
|
|
200
|
+
"decrementUserAttribute" -> decrementUserAttribute(getStringFromJson(args.getString(0)), args.optInt(1, 1))
|
|
188
201
|
"clearUserAttribute" -> clearUserAttribute(getStringFromJson(args.getString(0)))
|
|
189
202
|
"clearUserAttributes" -> clearUserAttributes()
|
|
190
203
|
"clearBuiltInAttributes" -> clearBuiltInAttributes()
|
|
204
|
+
"getBuiltInAttributes" -> getBuiltInAttributes(callbackContext)
|
|
205
|
+
"getBuiltInAttribute" -> getBuiltInAttribute(getStringFromJson(args.getString(0)), callbackContext)
|
|
191
206
|
"isEligibleForIntroOffer" -> isEligibleForIntroOffer(getStringFromJson(args.getString(0)), callbackContext)
|
|
192
207
|
"signPromotionalOffer" -> signPromotionalOffer(getStringFromJson(args.getString(0)), getStringFromJson(args.getString(1)), callbackContext)
|
|
193
208
|
"revokeDataProcessingConsent" -> revokeDataProcessingConsent(args.getJSONArray(0))
|
|
194
209
|
"setDebugMode" -> setDebugMode(args.getBoolean(0))
|
|
210
|
+
"setDynamicOffering" -> setDynamicOffering(
|
|
211
|
+
getStringFromJson(args.getString(0)),
|
|
212
|
+
getStringFromJson(args.getString(1)),
|
|
213
|
+
getStringFromJson(args.optString(2)),
|
|
214
|
+
callbackContext
|
|
215
|
+
)
|
|
216
|
+
"getDynamicOfferings" -> getDynamicOfferings(callbackContext)
|
|
217
|
+
"removeDynamicOffering" -> removeDynamicOffering(getStringFromJson(args.getString(0)))
|
|
218
|
+
"clearDynamicOfferings" -> clearDynamicOfferings()
|
|
195
219
|
else -> return false
|
|
196
220
|
}
|
|
197
221
|
} catch (e: JSONException) {
|
|
@@ -310,17 +334,6 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
310
334
|
return result
|
|
311
335
|
}
|
|
312
336
|
|
|
313
|
-
private fun close() {
|
|
314
|
-
defaultCallback = null
|
|
315
|
-
purchaseCallback = null
|
|
316
|
-
actionInterceptorCallbacks.clear()
|
|
317
|
-
pendingInterceptCompletions.clear()
|
|
318
|
-
displayedPresentation = null
|
|
319
|
-
// TODO(v6-verify): the global Purchasely.close() teardown was removed/lumped with
|
|
320
|
-
// presentation close in v6 (Flutter never calls it). Not invoked here to avoid a
|
|
321
|
-
// reference to a symbol that may no longer exist.
|
|
322
|
-
}
|
|
323
|
-
|
|
324
337
|
private fun addUserAttributesListener(callbackContext: CallbackContext) {
|
|
325
338
|
attributesCallback = callbackContext
|
|
326
339
|
Purchasely.userAttributeListener = object: UserAttributeListener {
|
|
@@ -383,6 +396,11 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
383
396
|
callbackContext.success(Purchasely.anonymousUserId)
|
|
384
397
|
}
|
|
385
398
|
|
|
399
|
+
// REC-12 / PAR-04
|
|
400
|
+
private fun isAnonymous(callbackContext: CallbackContext) {
|
|
401
|
+
callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, Purchasely.isAnonymous()))
|
|
402
|
+
}
|
|
403
|
+
|
|
386
404
|
private fun userLogin(userId: String?, callbackContext: CallbackContext) {
|
|
387
405
|
if(userId == null) {
|
|
388
406
|
callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, false))
|
|
@@ -394,12 +412,16 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
394
412
|
}
|
|
395
413
|
}
|
|
396
414
|
|
|
397
|
-
|
|
398
|
-
|
|
415
|
+
// PAR-30: clearUserAttributes defaults to true (matches the JS-side default).
|
|
416
|
+
private fun userLogout(clearUserAttributes: Boolean) {
|
|
417
|
+
Purchasely.userLogout(clearUserAttributes)
|
|
399
418
|
}
|
|
400
419
|
|
|
401
420
|
private fun setLogLevel(logLevel: Int) {
|
|
402
|
-
|
|
421
|
+
// CDV-W-13: values()[logLevel] throws ArrayIndexOutOfBoundsException for an
|
|
422
|
+
// out-of-range value, uncaught by execute()'s JSONException-only catch. Reuse the
|
|
423
|
+
// already-bounded helper start() relies on.
|
|
424
|
+
Purchasely.logLevel = logLevelFrom(logLevel)
|
|
403
425
|
}
|
|
404
426
|
|
|
405
427
|
private fun setLanguage(language: String?) {
|
|
@@ -447,6 +469,7 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
447
469
|
CordovaPLYAttribute.moengageUniqueId.ordinal -> Attribute.MOENGAGE_UNIQUE_ID
|
|
448
470
|
CordovaPLYAttribute.oneSignalExternalId.ordinal -> Attribute.ONESIGNAL_EXTERNAL_ID
|
|
449
471
|
CordovaPLYAttribute.batchCustomUserId.ordinal -> Attribute.BATCH_CUSTOM_USER_ID
|
|
472
|
+
CordovaPLYAttribute.oneSignalUserId.ordinal -> Attribute.ONESIGNAL_USER_ID
|
|
450
473
|
else -> null
|
|
451
474
|
}
|
|
452
475
|
|
|
@@ -503,7 +526,6 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
503
526
|
transition: JSONObject?,
|
|
504
527
|
callbackContext: CallbackContext
|
|
505
528
|
) {
|
|
506
|
-
purchaseCallback = callbackContext
|
|
507
529
|
displayPresentation(
|
|
508
530
|
screenId = presentationVendorId,
|
|
509
531
|
placementId = null,
|
|
@@ -519,7 +541,6 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
519
541
|
transition: JSONObject?,
|
|
520
542
|
callbackContext: CallbackContext
|
|
521
543
|
) {
|
|
522
|
-
purchaseCallback = callbackContext
|
|
523
544
|
displayPresentation(
|
|
524
545
|
screenId = null,
|
|
525
546
|
placementId = placementVendorId,
|
|
@@ -535,7 +556,6 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
535
556
|
transition: JSONObject?,
|
|
536
557
|
callbackContext: CallbackContext
|
|
537
558
|
) {
|
|
538
|
-
purchaseCallback = callbackContext
|
|
539
559
|
displayPresentation(
|
|
540
560
|
screenId = null,
|
|
541
561
|
placementId = null,
|
|
@@ -563,7 +583,9 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
563
583
|
contentId(contentId)
|
|
564
584
|
// v6: stream the presentation lifecycle to the JS callback via keep-alive
|
|
565
585
|
// envelopes; the dismiss outcome is delivered as the final (non-kept)
|
|
566
|
-
// result
|
|
586
|
+
// result, resolved directly against this invocation's own callbackContext
|
|
587
|
+
// (CDV-W-07: no shared/global callback slot, so overlapping present* calls
|
|
588
|
+
// can never cross-wire, matching iOS's per-invocation closure capture).
|
|
567
589
|
onPresented { presentation, error ->
|
|
568
590
|
val env = mutableMapOf<String, Any?>("event" to "presented")
|
|
569
591
|
presentation?.let { env["presentation"] = presentationToMap(it) }
|
|
@@ -581,10 +603,9 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
581
603
|
val loaded = builder.build().preload()
|
|
582
604
|
displayedPresentation = loaded
|
|
583
605
|
loaded.display(cordova.activity, transitionFromMap(transition)) { outcome ->
|
|
584
|
-
|
|
606
|
+
callbackContext.success(JSONObject(outcomeToMap(outcome)))
|
|
585
607
|
}
|
|
586
608
|
} catch (t: Throwable) {
|
|
587
|
-
if (purchaseCallback === callbackContext) purchaseCallback = null
|
|
588
609
|
callbackContext.error(t.message ?: "Unable to present presentation")
|
|
589
610
|
}
|
|
590
611
|
}
|
|
@@ -597,6 +618,13 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
597
618
|
callbackContext: CallbackContext) {
|
|
598
619
|
launch {
|
|
599
620
|
try {
|
|
621
|
+
// Synthetic handle so presentPresentation can re-display this preloaded
|
|
622
|
+
// presentation. Private re-display key: `fetchId`, NOT `id` -- `screenId`
|
|
623
|
+
// (already set by presentationToMap) is the sole authoritative public
|
|
624
|
+
// identifier. Generated up front so the onPresented/onCloseRequested closures
|
|
625
|
+
// below can capture it -- the native SDK fixes those on the builder, before
|
|
626
|
+
// preload(), so they must be seeded here rather than at re-display time.
|
|
627
|
+
val handle = "ply_fetch_${System.nanoTime()}"
|
|
600
628
|
val builder = PLYPresentationBase.builder().apply {
|
|
601
629
|
when {
|
|
602
630
|
placementId != null -> this.placementId(placementId)
|
|
@@ -604,13 +632,32 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
604
632
|
// else: neither id → the default (audience-targeted) presentation
|
|
605
633
|
}
|
|
606
634
|
contentId(contentId)
|
|
635
|
+
// v6: these fire on whichever invocation ends up re-displaying this
|
|
636
|
+
// presentation (via presentPresentation), not this fetch call -- resolve the
|
|
637
|
+
// live CallbackContext from lifecycleCallbacks lazily, at fire time, instead
|
|
638
|
+
// of capturing this fetch's own (long-finished) callbackContext. If nothing
|
|
639
|
+
// is registered yet (fired before any display), it's a silent no-op: there is
|
|
640
|
+
// no invocation to observe it.
|
|
641
|
+
onPresented { presentation, error ->
|
|
642
|
+
val cb = lifecycleCallbacks[handle] ?: return@onPresented
|
|
643
|
+
val env = mutableMapOf<String, Any?>("event" to "presented")
|
|
644
|
+
presentation?.let { env["presentation"] = presentationToMap(it) }
|
|
645
|
+
error?.let { env["error"] = it.message }
|
|
646
|
+
val r = PluginResult(PluginResult.Status.OK, JSONObject(env))
|
|
647
|
+
r.keepCallback = true
|
|
648
|
+
cb.sendPluginResult(r)
|
|
649
|
+
}
|
|
650
|
+
onCloseRequested {
|
|
651
|
+
val cb = lifecycleCallbacks[handle] ?: return@onCloseRequested
|
|
652
|
+
val r = PluginResult(PluginResult.Status.OK, JSONObject(mapOf("event" to "closeRequested")))
|
|
653
|
+
r.keepCallback = true
|
|
654
|
+
cb.sendPluginResult(r)
|
|
655
|
+
}
|
|
607
656
|
}
|
|
608
657
|
val loaded = builder.build().preload()
|
|
609
|
-
// Synthetic handle so presentPresentation can re-display this preloaded presentation.
|
|
610
|
-
val handle = "ply_fetch_${System.nanoTime()}"
|
|
611
658
|
loadedPresentations[handle] = loaded
|
|
612
659
|
val map = presentationToMap(loaded).toMutableMap()
|
|
613
|
-
map["
|
|
660
|
+
map["fetchId"] = handle
|
|
614
661
|
callbackContext.success(JSONObject(map))
|
|
615
662
|
} catch (t: Throwable) {
|
|
616
663
|
callbackContext.error(t.message ?: "Unable to fetch presentation")
|
|
@@ -627,23 +674,51 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
627
674
|
return
|
|
628
675
|
}
|
|
629
676
|
|
|
630
|
-
|
|
677
|
+
// Private re-display key: `fetchId` (see fetchPresentation above). `id` is tolerated
|
|
678
|
+
// as an internal belt-and-braces fallback only -- never a documented public key.
|
|
679
|
+
val handle = when {
|
|
680
|
+
presentationMap.has("fetchId") -> presentationMap.optString("fetchId")
|
|
681
|
+
presentationMap.has("id") -> presentationMap.optString("id")
|
|
682
|
+
else -> null
|
|
683
|
+
}
|
|
631
684
|
val loaded = handle?.let { loadedPresentations[it] }
|
|
632
|
-
if (loaded == null) {
|
|
685
|
+
if (loaded == null || handle == null) {
|
|
633
686
|
callbackContext.error("presentation cannot be found")
|
|
634
687
|
return
|
|
635
688
|
}
|
|
636
689
|
|
|
637
|
-
purchaseCallback = callbackContext
|
|
638
690
|
displayedPresentation = loaded
|
|
639
691
|
|
|
640
692
|
// TODO(v6-verify): loadingBackgroundColor cannot be applied to an already-preloaded
|
|
641
|
-
// presentation in v6 (colors are builder options set before preload).
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
693
|
+
// presentation in v6 (colors are builder options set before preload).
|
|
694
|
+
//
|
|
695
|
+
// v6: onPresented/onCloseRequested were seeded on the builder back in
|
|
696
|
+
// fetchPresentation() (fixed at build time, before preload -- the native SDK doesn't
|
|
697
|
+
// allow setting them later) and resolve their live CallbackContext from
|
|
698
|
+
// lifecycleCallbacks lazily, at fire time. Register THIS invocation's callbackContext
|
|
699
|
+
// under `handle` before triggering display() so those events reach it, matching the
|
|
700
|
+
// direct present* path's envelopes exactly (same event/presentation/error keys,
|
|
701
|
+
// keepCallback=true). Confirmed against the native SDK sources: display(callback) only
|
|
702
|
+
// replaces onDismissed, never onPresented/onCloseRequested (PLYPresentationBase.
|
|
703
|
+
// dispatchDisplay).
|
|
704
|
+
//
|
|
705
|
+
// CDV-W-07: resolve directly against this invocation's own callbackContext (no shared
|
|
706
|
+
// slot). Remove the registration as soon as this invocation's dismiss outcome is
|
|
707
|
+
// delivered, or immediately on any early-return error below -- otherwise a failed
|
|
708
|
+
// display leaves a stale entry routing future lifecycle events to a callback that will
|
|
709
|
+
// never fire again.
|
|
710
|
+
lifecycleCallbacks[handle] = callbackContext
|
|
711
|
+
|
|
712
|
+
val activity = cordova.activity
|
|
713
|
+
if (activity == null) {
|
|
714
|
+
lifecycleCallbacks.remove(handle)
|
|
715
|
+
callbackContext.error("No activity available to display presentation")
|
|
716
|
+
return
|
|
717
|
+
}
|
|
718
|
+
activity.runOnUiThread {
|
|
719
|
+
loaded.display(activity, transitionFromMap(transition)) { outcome ->
|
|
720
|
+
lifecycleCallbacks.remove(handle)
|
|
721
|
+
callbackContext.success(JSONObject(outcomeToMap(outcome)))
|
|
647
722
|
}
|
|
648
723
|
}
|
|
649
724
|
}
|
|
@@ -710,10 +785,11 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
710
785
|
)
|
|
711
786
|
}
|
|
712
787
|
|
|
713
|
-
|
|
788
|
+
// PAR-29: invalidateCache defaults to false (exposed from JS; was hardcoded true).
|
|
789
|
+
private fun userSubscriptions(invalidateCache: Boolean, callbackContext: CallbackContext) {
|
|
714
790
|
launch {
|
|
715
791
|
try {
|
|
716
|
-
val list = Purchasely.userSubscriptions(
|
|
792
|
+
val list = Purchasely.userSubscriptions(invalidateCache)
|
|
717
793
|
callbackContext.success(transformSubscriptionsToJson(list))
|
|
718
794
|
} catch (e: Exception) {
|
|
719
795
|
callbackContext.error(e.message)
|
|
@@ -721,10 +797,10 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
721
797
|
}
|
|
722
798
|
}
|
|
723
799
|
|
|
724
|
-
private fun userSubscriptionsHistory(callbackContext: CallbackContext) {
|
|
800
|
+
private fun userSubscriptionsHistory(invalidateCache: Boolean, callbackContext: CallbackContext) {
|
|
725
801
|
launch {
|
|
726
802
|
try {
|
|
727
|
-
val list = Purchasely.userSubscriptionsHistory(
|
|
803
|
+
val list = Purchasely.userSubscriptionsHistory(invalidateCache)
|
|
728
804
|
callbackContext.success(transformSubscriptionsToJson(list))
|
|
729
805
|
} catch (e: Exception) {
|
|
730
806
|
callbackContext.error(e.message)
|
|
@@ -732,17 +808,42 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
732
808
|
}
|
|
733
809
|
}
|
|
734
810
|
|
|
811
|
+
// Hardening for the upcoming rc.4 native release: PLYPlan.toMap()'s raw "type" entry is
|
|
812
|
+
// moving from an ordinal (Int) to the DistributionType name (String). transformPlanToMap
|
|
813
|
+
// already overwrites "type" explicitly wherever it's used, but allProducts/
|
|
814
|
+
// productWithIdentifier/the subscription's nested "product" field pass product.toMap()
|
|
815
|
+
// straight through -- normalize those raw plan entries so the JS PlanType contract
|
|
816
|
+
// (an ordinal) stays stable across both native formats. Both formats resolve to the same
|
|
817
|
+
// ordinal since DistributionType's declared order already matches Purchasely.PlanType.
|
|
818
|
+
private fun normalizePlanTypeOrdinal(raw: Any?): Int? = when (raw) {
|
|
819
|
+
is Number -> raw.toInt()
|
|
820
|
+
is String -> runCatching { DistributionType.valueOf(raw).ordinal }.getOrNull()
|
|
821
|
+
else -> null
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private fun normalizeProductPlans(map: Map<String, Any?>): Map<String, Any?> {
|
|
825
|
+
val plans = map["plans"] as? List<*> ?: return map
|
|
826
|
+
val normalized = plans.map { plan ->
|
|
827
|
+
val planMap = plan as? Map<*, *> ?: return@map plan
|
|
828
|
+
HashMap(planMap).apply { this["type"] = normalizePlanTypeOrdinal(this["type"]) }
|
|
829
|
+
}
|
|
830
|
+
return HashMap(map).apply { this["plans"] = normalized }
|
|
831
|
+
}
|
|
832
|
+
|
|
735
833
|
private fun transformSubscriptionsToJson(list: List<PLYSubscriptionData>): JSONArray {
|
|
736
834
|
val result = JSONArray()
|
|
737
835
|
for (data in list) {
|
|
738
836
|
val map = HashMap(data.data.toMap())
|
|
739
837
|
map["plan"] = transformPlanToMap(data.plan)
|
|
740
|
-
map["product"] = data.product.toMap()
|
|
838
|
+
map["product"] = normalizeProductPlans(data.product.toMap())
|
|
741
839
|
map["subscriptionSource"] = when (data.data.storeType) {
|
|
742
840
|
StoreType.GOOGLE_PLAY_STORE -> StoreType.GOOGLE_PLAY_STORE.ordinal
|
|
743
841
|
StoreType.AMAZON_APP_STORE -> StoreType.AMAZON_APP_STORE.ordinal
|
|
744
842
|
StoreType.HUAWEI_APP_GALLERY -> StoreType.HUAWEI_APP_GALLERY.ordinal
|
|
745
843
|
StoreType.APPLE_APP_STORE -> StoreType.APPLE_APP_STORE.ordinal
|
|
844
|
+
// CDV-W-15: NONE/WEB_CHECKOUT_STRIPE have no JS SubscriptionSource case of
|
|
845
|
+
// their own; both map to `none` (4), matching iOS's PLYSubscriptionSource.None.
|
|
846
|
+
StoreType.NONE, StoreType.WEB_CHECKOUT_STRIPE -> 4
|
|
746
847
|
else -> null
|
|
747
848
|
}
|
|
748
849
|
result.put(JSONObject(map))
|
|
@@ -770,7 +871,7 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
770
871
|
val list = Purchasely.allProducts()
|
|
771
872
|
val result = JSONArray()
|
|
772
873
|
for (product in list) {
|
|
773
|
-
result.put(JSONObject(product.toMap()))
|
|
874
|
+
result.put(JSONObject(normalizeProductPlans(product.toMap())))
|
|
774
875
|
}
|
|
775
876
|
callbackContext.success(result)
|
|
776
877
|
} catch (e: Exception) {
|
|
@@ -788,7 +889,7 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
788
889
|
try {
|
|
789
890
|
val product: PLYProduct? = Purchasely.product(vendorId)
|
|
790
891
|
if (product != null) {
|
|
791
|
-
callbackContext.success(JSONObject(product.toMap()))
|
|
892
|
+
callbackContext.success(JSONObject(normalizeProductPlans(product.toMap())))
|
|
792
893
|
} else {
|
|
793
894
|
callbackContext.error("No product found with $vendorId")
|
|
794
895
|
}
|
|
@@ -1086,11 +1187,36 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1086
1187
|
is JSONArray -> callbackContext.success(result)
|
|
1087
1188
|
is String -> callbackContext.success(result)
|
|
1088
1189
|
is Int -> callbackContext.success(result)
|
|
1089
|
-
|
|
1190
|
+
// CDV-W-06: getUserAttributeValueForCordova converts a Float attribute to a Double
|
|
1191
|
+
// (to preserve precision); this branch was missing, so reading back any
|
|
1192
|
+
// setUserAttributeWithDouble value always fell through to the error case below.
|
|
1193
|
+
is Double -> callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, result.toFloat()))
|
|
1194
|
+
// CDV-W-09: send a real JSON boolean (matches iOS) instead of 0/1.
|
|
1195
|
+
is Boolean -> callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, result))
|
|
1090
1196
|
else -> callbackContext.error("No user attribute found with $key")
|
|
1091
1197
|
}
|
|
1092
1198
|
}
|
|
1093
1199
|
|
|
1200
|
+
// REC-12 / PAR-03: bulk read, same per-value conversion as the single-key read above.
|
|
1201
|
+
private fun userAttributes(callbackContext: CallbackContext) {
|
|
1202
|
+
val result = JSONObject()
|
|
1203
|
+
Purchasely.userAttributes().forEach { (key, value) ->
|
|
1204
|
+
result.put(key, getUserAttributeValueForCordova(value))
|
|
1205
|
+
}
|
|
1206
|
+
callbackContext.success(result)
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// REC-12 / PAR-02
|
|
1210
|
+
private fun incrementUserAttribute(key: String?, value: Int) {
|
|
1211
|
+
if (key == null) return
|
|
1212
|
+
Purchasely.incrementUserAttribute(key, value)
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
private fun decrementUserAttribute(key: String?, value: Int) {
|
|
1216
|
+
if (key == null) return
|
|
1217
|
+
Purchasely.decrementUserAttribute(key, value)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1094
1220
|
private fun getUserAttributeValueForCordova(value: Any?): Any? {
|
|
1095
1221
|
return when (value) {
|
|
1096
1222
|
is Date -> {
|
|
@@ -1135,6 +1261,33 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1135
1261
|
Purchasely.clearBuiltInAttributes()
|
|
1136
1262
|
}
|
|
1137
1263
|
|
|
1264
|
+
// PAR-07
|
|
1265
|
+
private fun getBuiltInAttributes(callbackContext: CallbackContext) {
|
|
1266
|
+
val result = JSONObject()
|
|
1267
|
+
Purchasely.getBuiltInAttributes().forEach { (key, value) ->
|
|
1268
|
+
result.put(key, getUserAttributeValueForCordova(value))
|
|
1269
|
+
}
|
|
1270
|
+
callbackContext.success(result)
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
private fun getBuiltInAttribute(key: String?, callbackContext: CallbackContext) {
|
|
1274
|
+
if (key == null) {
|
|
1275
|
+
callbackContext.success()
|
|
1276
|
+
return
|
|
1277
|
+
}
|
|
1278
|
+
val value = getUserAttributeValueForCordova(Purchasely.getBuiltInAttribute(key))
|
|
1279
|
+
when (value) {
|
|
1280
|
+
is JSONArray -> callbackContext.success(value)
|
|
1281
|
+
is String -> callbackContext.success(value)
|
|
1282
|
+
is Int -> callbackContext.success(value)
|
|
1283
|
+
is Double -> callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, value.toFloat()))
|
|
1284
|
+
is Boolean -> callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, value))
|
|
1285
|
+
// No attribute for this key: resolve success with no value (undefined in JS),
|
|
1286
|
+
// matching iOS's nullable id? return.
|
|
1287
|
+
else -> callbackContext.success()
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1138
1291
|
private fun isEligibleForIntroOffer(planId: String?, callbackContext: CallbackContext) {
|
|
1139
1292
|
launch {
|
|
1140
1293
|
try {
|
|
@@ -1147,8 +1300,47 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1147
1300
|
}
|
|
1148
1301
|
}
|
|
1149
1302
|
|
|
1303
|
+
// REC-04: iOS-only feature (StoreKit promotional offer signing). No error on Android by
|
|
1304
|
+
// design (Kevin's call) -- signPromotionalOffer resolves as a no-op success so shared JS
|
|
1305
|
+
// calling it unconditionally on both platforms doesn't have to special-case Android.
|
|
1150
1306
|
private fun signPromotionalOffer(storeProductId: String?, storeOfferId: String?, callbackContext: CallbackContext) {
|
|
1151
|
-
callbackContext.
|
|
1307
|
+
callbackContext.success()
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// PAR-05: Dynamic Offerings. Payload keys (reference/planVendorId/offerVendorId) match
|
|
1311
|
+
// the Cordova JS↔native contract (not the native SDK's own reference/planId/offerId
|
|
1312
|
+
// property names on PLYDynamicOffering).
|
|
1313
|
+
private fun setDynamicOffering(reference: String?, planVendorId: String?, offerVendorId: String?, callbackContext: CallbackContext) {
|
|
1314
|
+
if (reference == null || planVendorId == null) {
|
|
1315
|
+
callbackContext.error("reference and planVendorId are required")
|
|
1316
|
+
return
|
|
1317
|
+
}
|
|
1318
|
+
Purchasely.setDynamicOffering(reference, planVendorId, offerVendorId) { success ->
|
|
1319
|
+
callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, success))
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
private fun getDynamicOfferings(callbackContext: CallbackContext) {
|
|
1324
|
+
Purchasely.getDynamicOfferings { offerings ->
|
|
1325
|
+
val result = JSONArray()
|
|
1326
|
+
offerings.forEach { offering ->
|
|
1327
|
+
result.put(JSONObject(mapOf(
|
|
1328
|
+
"reference" to offering.reference,
|
|
1329
|
+
"planVendorId" to offering.planId,
|
|
1330
|
+
"offerVendorId" to (offering.offerId ?: JSONObject.NULL)
|
|
1331
|
+
)))
|
|
1332
|
+
}
|
|
1333
|
+
callbackContext.success(result)
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private fun removeDynamicOffering(reference: String?) {
|
|
1338
|
+
if (reference == null) return
|
|
1339
|
+
Purchasely.removeDynamicOffering(reference)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
private fun clearDynamicOfferings() {
|
|
1343
|
+
Purchasely.clearDynamicOfferings()
|
|
1152
1344
|
}
|
|
1153
1345
|
|
|
1154
1346
|
private fun revokeDataProcessingConsent(purposes: JSONArray?) {
|
|
@@ -1180,27 +1372,9 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1180
1372
|
|
|
1181
1373
|
companion object {
|
|
1182
1374
|
var defaultCallback: CallbackContext? = null
|
|
1183
|
-
var purchaseCallback: CallbackContext? = null
|
|
1184
1375
|
var eventsCallback: CallbackContext? = null
|
|
1185
1376
|
var attributesCallback: CallbackContext? = null
|
|
1186
1377
|
|
|
1187
|
-
// Resolves a present*/presentPresentation call at dismissal. One-shot for the
|
|
1188
|
-
// purchase callback; falls back to the (repeatable) default dismiss handler.
|
|
1189
|
-
fun sendPurchaseResult(outcome: PLYPresentationOutcome) {
|
|
1190
|
-
val json = JSONObject(outcomeToMap(outcome))
|
|
1191
|
-
val oneShot = purchaseCallback
|
|
1192
|
-
if (oneShot != null) {
|
|
1193
|
-
oneShot.success(json)
|
|
1194
|
-
purchaseCallback = null
|
|
1195
|
-
return
|
|
1196
|
-
}
|
|
1197
|
-
defaultCallback?.let {
|
|
1198
|
-
val pluginResult = PluginResult(PluginResult.Status.OK, json)
|
|
1199
|
-
pluginResult.keepCallback = true
|
|
1200
|
-
it.sendPluginResult(pluginResult)
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
1378
|
// Serializes a v6 PLYPresentationOutcome to the wire contract. `result` is kept as an
|
|
1205
1379
|
// int (PurchaseResult 0/1/2) for back-compat with the pre-6.0 JS layer.
|
|
1206
1380
|
fun outcomeToMap(outcome: PLYPresentationOutcome): Map<String?, Any?> {
|
|
@@ -1215,7 +1389,11 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1215
1389
|
// v6: also expose the string purchaseResult ('purchased'|'cancelled'|'restored'|null)
|
|
1216
1390
|
// alongside the legacy int `result`, matching the Flutter outcome contract.
|
|
1217
1391
|
map["purchaseResult"] = outcome.purchaseResult?.name?.lowercase(Locale.US)
|
|
1218
|
-
|
|
1392
|
+
// CDV-W-05: transformPlanToMap(null) returns {} (not null), so setting the key
|
|
1393
|
+
// unconditionally made a naive `if (outcome.plan)` truthy check on Android but
|
|
1394
|
+
// falsy (key omitted) on iOS for the same no-purchase dismissal. Omit the key
|
|
1395
|
+
// entirely when there is no plan, matching iOS.
|
|
1396
|
+
outcome.plan?.let { map["plan"] = transformPlanToMap(it) }
|
|
1219
1397
|
map["closeReason"] = outcome.closeReason?.value
|
|
1220
1398
|
map["error"] = outcome.error?.message
|
|
1221
1399
|
map["presentation"] = outcome.presentation?.let { presentationToMap(it) }
|
|
@@ -1264,7 +1442,8 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1264
1442
|
}
|
|
1265
1443
|
}
|
|
1266
1444
|
|
|
1267
|
-
// WARNING: This enum must be strictly identical to the one in
|
|
1445
|
+
// WARNING: This enum must be strictly identical (same declaration order) to the one in
|
|
1446
|
+
// the JS side (Purchasely.js) and iOS's CordovaPLYAttribute typedef.
|
|
1268
1447
|
enum class CordovaPLYAttribute {
|
|
1269
1448
|
firebase_app_instance_id,
|
|
1270
1449
|
airship_channel_id,
|
|
@@ -1287,6 +1466,7 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1287
1466
|
moengageUniqueId,
|
|
1288
1467
|
oneSignalExternalId,
|
|
1289
1468
|
batchCustomUserId,
|
|
1469
|
+
oneSignalUserId,
|
|
1290
1470
|
|
|
1291
1471
|
/*
|
|
1292
1472
|
FIREBASE_APP_INSTANCE_ID: 0,
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
- (void)eventTriggered:(enum PLYEvent)event properties:(NSDictionary<NSString *, id> * _Nullable)properties {
|
|
15
15
|
if (self.eventCommand) {
|
|
16
|
-
|
|
16
|
+
// CDV-W-01: properties is _Nullable; inserting nil into an ObjC dictionary LITERAL
|
|
17
|
+
// throws NSInvalidArgumentException. Build it mutably and only set the key when non-nil.
|
|
18
|
+
NSMutableDictionary<NSString *, id> *eventDict = [NSMutableDictionary new];
|
|
19
|
+
[eventDict setObject:[NSString fromPLYEvent:event] forKey:@"name"];
|
|
20
|
+
if (properties != nil) {
|
|
21
|
+
[eventDict setObject:properties forKey:@"properties"];
|
|
22
|
+
}
|
|
17
23
|
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:eventDict];
|
|
18
24
|
|
|
19
25
|
[pluginResult setKeepCallbackAsBool:YES];
|
package/src/ios/CDVPurchasely.h
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
- (void)userLogout:(CDVInvokedUrlCommand*)command;
|
|
39
39
|
- (void)setAttribute:(CDVInvokedUrlCommand*)command;
|
|
40
40
|
- (void)getAnonymousUserId:(CDVInvokedUrlCommand*)command;
|
|
41
|
+
- (void)isAnonymous:(CDVInvokedUrlCommand*)command;
|
|
41
42
|
- (void)allowDeeplink:(CDVInvokedUrlCommand*)command;
|
|
42
43
|
- (void)allowCampaigns:(CDVInvokedUrlCommand*)command;
|
|
43
44
|
- (void)handleDeeplink:(CDVInvokedUrlCommand*)command;
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
- (void)unregisterActionInterceptor:(CDVInvokedUrlCommand*)command;
|
|
61
62
|
- (void)completeActionInterceptor:(CDVInvokedUrlCommand*)command;
|
|
62
63
|
- (void)closePresentation:(CDVInvokedUrlCommand*)command;
|
|
64
|
+
- (void)closeAllScreens:(CDVInvokedUrlCommand*)command;
|
|
63
65
|
- (void)backPresentation:(CDVInvokedUrlCommand*)command;
|
|
64
66
|
- (void)userDidConsumeSubscriptionContent:(CDVInvokedUrlCommand*)command;
|
|
65
67
|
- (void)setUserAttributeWithStringArray:(CDVInvokedUrlCommand*)command;
|
|
@@ -72,14 +74,23 @@
|
|
|
72
74
|
- (void)setUserAttributeWithDouble:(CDVInvokedUrlCommand*)command;
|
|
73
75
|
- (void)setUserAttributeWithDate:(CDVInvokedUrlCommand*)command;
|
|
74
76
|
- (void)userAttribute:(CDVInvokedUrlCommand*)command;
|
|
77
|
+
- (void)userAttributes:(CDVInvokedUrlCommand*)command;
|
|
78
|
+
- (void)incrementUserAttribute:(CDVInvokedUrlCommand*)command;
|
|
79
|
+
- (void)decrementUserAttribute:(CDVInvokedUrlCommand*)command;
|
|
75
80
|
- (void)clearUserAttribute:(CDVInvokedUrlCommand*)command;
|
|
76
81
|
- (void)clearUserAttributes:(CDVInvokedUrlCommand*)command;
|
|
77
82
|
- (void)clearBuiltInAttributes:(CDVInvokedUrlCommand*)command;
|
|
83
|
+
- (void)getBuiltInAttributes:(CDVInvokedUrlCommand*)command;
|
|
84
|
+
- (void)getBuiltInAttribute:(CDVInvokedUrlCommand*)command;
|
|
78
85
|
- (void)fetchPresentation:(CDVInvokedUrlCommand*)command;
|
|
79
86
|
- (void)presentPresentation:(CDVInvokedUrlCommand*)command;
|
|
80
87
|
- (void)signPromotionalOffer:(CDVInvokedUrlCommand*)command;
|
|
81
88
|
- (void)isEligibleForIntroOffer:(CDVInvokedUrlCommand*)command;
|
|
82
89
|
- (void)setThemeMode:(CDVInvokedUrlCommand*)command;
|
|
83
90
|
- (void)addUserAttributeListener:(CDVInvokedUrlCommand*)command;
|
|
91
|
+
- (void)setDynamicOffering:(CDVInvokedUrlCommand*)command;
|
|
92
|
+
- (void)getDynamicOfferings:(CDVInvokedUrlCommand*)command;
|
|
93
|
+
- (void)removeDynamicOffering:(CDVInvokedUrlCommand*)command;
|
|
94
|
+
- (void)clearDynamicOfferings:(CDVInvokedUrlCommand*)command;
|
|
84
95
|
|
|
85
96
|
@end
|