@purchasely/cordova-plugin-purchasely 6.0.1 → 6.1.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 +1 -1
- package/package.json +7 -2
- package/plugin.xml +3 -3
- package/src/android/PurchaselyPlugin.kt +366 -36
- package/src/ios/CDVPurchasely+Events.h +1 -1
- package/src/ios/CDVPurchasely+Events.m +34 -0
- package/src/ios/CDVPurchasely.h +57 -0
- package/src/ios/CDVPurchasely.m +173 -0
- package/www/Purchasely.js +132 -2
package/Package.swift
CHANGED
|
@@ -56,7 +56,7 @@ let package = Package(
|
|
|
56
56
|
// documents another. That makes the linked SDK depend on install time and lets
|
|
57
57
|
// the two integration paths drift apart. Bump this with the podspec, in the
|
|
58
58
|
// same commit, and VERSIONS.md with it.
|
|
59
|
-
.package(url: "https://github.com/Purchasely/Purchasely-iOS.git", exact: "6.
|
|
59
|
+
.package(url: "https://github.com/Purchasely/Purchasely-iOS.git", exact: "6.1.2")
|
|
60
60
|
],
|
|
61
61
|
targets: [
|
|
62
62
|
.target(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@purchasely/cordova-plugin-purchasely",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.1",
|
|
4
4
|
"description": "Purchasely is a solution to ease the integration and boost your In-App Purchases & Subscriptions on the App Store, Google Play Store, Amazon Appstore and Huawei App Gallery.",
|
|
5
5
|
"cordova": {
|
|
6
6
|
"id": "@purchasely/cordova-plugin-purchasely",
|
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
},
|
|
66
66
|
"homepage": "https://github.com/Purchasely/Purchasely-Cordova#readme",
|
|
67
67
|
"overrides": {
|
|
68
|
-
"js-yaml@3": "^3.15.
|
|
68
|
+
"js-yaml@3": "^3.15.2",
|
|
69
|
+
"browserslist": "^4.28.7"
|
|
70
|
+
},
|
|
71
|
+
"resolutions": {
|
|
72
|
+
"js-yaml": "^3.15.2",
|
|
73
|
+
"browserslist": "^4.28.7"
|
|
69
74
|
}
|
|
70
75
|
}
|
package/plugin.xml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<?xml version='1.0' encoding='utf-8'?>
|
|
2
|
-
<plugin id="@purchasely/cordova-plugin-purchasely" version="6.
|
|
2
|
+
<plugin id="@purchasely/cordova-plugin-purchasely" version="6.1.1" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
3
|
<name>Purchasely</name>
|
|
4
4
|
<js-module name="Purchasely" src="www/Purchasely.js">
|
|
5
5
|
<clobbers target="Purchasely" />
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
<source url="https://github.com/CocoaPods/Specs.git"/>
|
|
39
39
|
</config>
|
|
40
40
|
<pods use-frameworks="true">
|
|
41
|
-
<pod name="Purchasely" spec="6.
|
|
41
|
+
<pod name="Purchasely" spec="6.1.2" nospm="true"/>
|
|
42
42
|
</pods>
|
|
43
43
|
</podspec>
|
|
44
44
|
</platform>
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
<preference name="GradlePluginKotlinCodeStyle" value="official" />
|
|
54
54
|
</config-file>
|
|
55
55
|
<framework src="src/android/build-extras.gradle" custom="true" type="gradleReference" />
|
|
56
|
-
<framework src="io.purchasely:core:6.
|
|
56
|
+
<framework src="io.purchasely:core:6.1.1" />
|
|
57
57
|
<source-file src="src/android/PurchaselyPlugin.kt" target-dir="java/cordova/plugin/purchasely" />
|
|
58
58
|
</platform>
|
|
59
59
|
</plugin>
|
|
@@ -16,6 +16,7 @@ import io.purchasely.ext.PLYEvent
|
|
|
16
16
|
import io.purchasely.ext.PLYInterceptResult
|
|
17
17
|
import io.purchasely.ext.PLYInterceptorInfo
|
|
18
18
|
import io.purchasely.ext.PLYRunningMode
|
|
19
|
+
import io.purchasely.ext.PLYWebRedemptionListener
|
|
19
20
|
import io.purchasely.ext.PurchaseListener
|
|
20
21
|
import io.purchasely.ext.Purchasely
|
|
21
22
|
import io.purchasely.ext.State
|
|
@@ -33,6 +34,7 @@ import io.purchasely.models.PLYPlan
|
|
|
33
34
|
import io.purchasely.models.PLYPresentationPlan
|
|
34
35
|
import io.purchasely.models.PLYProduct
|
|
35
36
|
import io.purchasely.models.PLYSubscriptionData
|
|
37
|
+
import io.purchasely.models.PLYWebRedemptionResult
|
|
36
38
|
import io.purchasely.views.presentation.PLYThemeMode
|
|
37
39
|
import io.purchasely.views.presentation.models.PLYDimensionType
|
|
38
40
|
import io.purchasely.views.presentation.models.PLYTransition
|
|
@@ -53,6 +55,9 @@ import java.util.Calendar
|
|
|
53
55
|
import java.util.Date
|
|
54
56
|
import java.util.Locale
|
|
55
57
|
import java.util.TimeZone
|
|
58
|
+
import java.util.UUID
|
|
59
|
+
import java.net.URI
|
|
60
|
+
import java.net.URISyntaxException
|
|
56
61
|
import java.util.concurrent.ConcurrentHashMap
|
|
57
62
|
|
|
58
63
|
/**
|
|
@@ -87,9 +92,43 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
87
92
|
|
|
88
93
|
override fun onDestroy() {
|
|
89
94
|
job.cancel()
|
|
95
|
+
clearCallbackContexts()
|
|
90
96
|
super.onDestroy()
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Cordova calls this when the WebView navigates, which invalidates every callbackId the
|
|
101
|
+
* previous page handed us.
|
|
102
|
+
*/
|
|
103
|
+
override fun onReset() {
|
|
104
|
+
clearCallbackContexts()
|
|
105
|
+
super.onReset()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Drop every stored [CallbackContext].
|
|
110
|
+
*
|
|
111
|
+
* These live on the companion object, so they are STATIC: they outlive both the plugin
|
|
112
|
+
* instance and the WebView. Neither teardown path cleared them before, which left two
|
|
113
|
+
* problems. Each stale context holds a reference to the dead CordovaWebView, so the
|
|
114
|
+
* whole view tree leaked. And the native SDK listeners registered at `start()` outlive
|
|
115
|
+
* the activity, so an event arriving after a teardown was sent into a dead bridge
|
|
116
|
+
* instead of being dropped.
|
|
117
|
+
*
|
|
118
|
+
* The redemption case is the one that matters most. `webRedemptionListener` is set on
|
|
119
|
+
* the builder, and `Purchasely.Builder.build()` REASSIGNS the SDK's static listener on
|
|
120
|
+
* every call, so a page reload that starts again replaces the lambda rather than
|
|
121
|
+
* stacking one. Until it does, the lambda from the previous plugin instance is still
|
|
122
|
+
* live and reads `webRedemptionCallback` lazily, at fire time. Clearing here is what
|
|
123
|
+
* makes that window a clean no-op rather than a send into a dead callbackId.
|
|
124
|
+
*/
|
|
125
|
+
private fun clearCallbackContexts() {
|
|
126
|
+
defaultCallback = null
|
|
127
|
+
eventsCallback = null
|
|
128
|
+
attributesCallback = null
|
|
129
|
+
webRedemptionCallback = null
|
|
130
|
+
}
|
|
131
|
+
|
|
93
132
|
override fun execute(
|
|
94
133
|
action: String,
|
|
95
134
|
args: JSONArray,
|
|
@@ -103,6 +142,8 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
103
142
|
"addUserAttributeListener" -> addUserAttributesListener(callbackContext)
|
|
104
143
|
"removeUserAttributeListener" -> removeUserAttributesListener()
|
|
105
144
|
"removeEventsListener" -> removeEventsListener()
|
|
145
|
+
"addWebRedemptionListener" -> addWebRedemptionListener(callbackContext)
|
|
146
|
+
"removeWebRedemptionListener" -> removeWebRedemptionListener(callbackContext)
|
|
106
147
|
"getAnonymousUserId" -> getAnonymousUserId(callbackContext)
|
|
107
148
|
"isAnonymous" -> isAnonymous(callbackContext)
|
|
108
149
|
"userLogin" -> userLogin(getStringFromJson(args.getString(0)), callbackContext)
|
|
@@ -281,6 +322,37 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
281
322
|
val allowCampaigns = if (options.has("allowCampaigns")) options.optBoolean("allowCampaigns") else null
|
|
282
323
|
val deeplink = getStringFromJson(options.optString("deeplink"))
|
|
283
324
|
val sdkVersion = getStringFromJson(options.optString("sdkVersion"))
|
|
325
|
+
// Both bridges report a refused-and-skipped option at the SAME severity, and
|
|
326
|
+
// deliberately with a plain log line on both: Log.e here, NSLog on iOS. Neither
|
|
327
|
+
// renders UI. Do not reach for anything that puts an overlay or a dialog in front
|
|
328
|
+
// of the host app: start() continues, the option was simply ignored, and a
|
|
329
|
+
// third-party SDK has no business interrupting someone else's app over an option
|
|
330
|
+
// it chose to skip.
|
|
331
|
+
// v6.1.0: three states, and they are not interchangeable. See [resolveProxyOption].
|
|
332
|
+
val proxyOption = resolveProxyOption(options)
|
|
333
|
+
if (proxyOption is PLYProxyOption.Invalid) {
|
|
334
|
+
Log.e("Purchasely", "`proxy` must be an https base URL, for example " +
|
|
335
|
+
"\"https://svc.purchasely.io\", or null to clear the proxy. Received a " +
|
|
336
|
+
"${proxyOption.rawValue?.length ?: 0}-character value. The proxy is not applied.")
|
|
337
|
+
}
|
|
338
|
+
val appHandlesRedemptionAlert = options.optBoolean("appHandlesRedemptionAlert", false)
|
|
339
|
+
|
|
340
|
+
// v6.1.0: JS has no UUID type, so the id crosses the bridge as a string and is parsed
|
|
341
|
+
// here. The native builder takes a UUID?, which is where the guarantee used to live; a
|
|
342
|
+
// string-typed bridge is the only place left to catch a bad value. Refuse it loudly and
|
|
343
|
+
// skip the option. The SDK still starts, matching how native treats an unusable proxy url.
|
|
344
|
+
val anonymousUserIdString = getStringFromJson(options.optString("anonymousUserId"))
|
|
345
|
+
val anonymousUserId = parseCanonicalUuid(anonymousUserIdString)
|
|
346
|
+
if (anonymousUserIdString != null && anonymousUserId == null) {
|
|
347
|
+
// The value is NOT logged. A mis-wired field lands here just as easily as a
|
|
348
|
+
// typo -- an email, an appUserId -- and logcat is collected during support. The
|
|
349
|
+
// length still distinguishes a truncated id from a wrong field. Matches iOS.
|
|
350
|
+
Log.e("Purchasely", "`anonymousUserId` must be a canonical UUID string, for example " +
|
|
351
|
+
"\"3f2504e0-4f89-11d3-9a0c-0305e82c3301\". Received a " +
|
|
352
|
+
"${anonymousUserIdString.length}-character value. " +
|
|
353
|
+
"The anonymous user id is not applied.")
|
|
354
|
+
}
|
|
355
|
+
val anonymousUserIdOverride = options.optBoolean("anonymousUserIdOverride", false)
|
|
284
356
|
|
|
285
357
|
Purchasely.Builder(cordova.context)
|
|
286
358
|
.apiKey(apiKey)
|
|
@@ -293,6 +365,22 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
293
365
|
allowCampaigns?.let { this.allowCampaigns(it) }
|
|
294
366
|
// Cold-start deeplink: replayed automatically once started.
|
|
295
367
|
deeplink?.let { this.handleDeeplink(Uri.parse(it)) }
|
|
368
|
+
// The three 6.1.0 options are applied through a seam so a unit test can
|
|
369
|
+
// verify WHICH builder call each resolver state produces. Without it,
|
|
370
|
+
// swapping the Clear and Set branches passed every test in the repository.
|
|
371
|
+
applyStartOptions(
|
|
372
|
+
builder = this,
|
|
373
|
+
proxyOption = proxyOption,
|
|
374
|
+
anonymousUserId = anonymousUserId,
|
|
375
|
+
anonymousUserIdOverride = anonymousUserIdOverride,
|
|
376
|
+
appHandlesRedemptionAlert = appHandlesRedemptionAlert,
|
|
377
|
+
// Registered unconditionally: the native SDK has no runtime setter on
|
|
378
|
+
// purpose, because a redemption can settle during start(). It sends
|
|
379
|
+
// nothing when addWebRedemptionListener recorded no callback.
|
|
380
|
+
redemptionListener = PLYWebRedemptionListener { result ->
|
|
381
|
+
deliverRedemption(result)
|
|
382
|
+
},
|
|
383
|
+
)
|
|
296
384
|
}
|
|
297
385
|
.build()
|
|
298
386
|
|
|
@@ -392,6 +480,65 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
392
480
|
Purchasely.eventListener = null
|
|
393
481
|
}
|
|
394
482
|
|
|
483
|
+
/**
|
|
484
|
+
* v6.1.0. Record the callback the redemption outcome is routed to.
|
|
485
|
+
*
|
|
486
|
+
* The native [PLYWebRedemptionListener] is registered on the builder chain in [start] (the
|
|
487
|
+
* SDK has no runtime setter, because a redemption can settle during `start()`), so this
|
|
488
|
+
* action only records where to send the outcome. JS must call it BEFORE `start()`.
|
|
489
|
+
*/
|
|
490
|
+
private fun addWebRedemptionListener(callbackContext: CallbackContext) {
|
|
491
|
+
// Close the previous stream before replacing it, or its JS closure stays in
|
|
492
|
+
// cordova.callbacks forever. See [releaseCallbackStream].
|
|
493
|
+
releaseCallbackStream(webRedemptionCallback)
|
|
494
|
+
webRedemptionCallback = callbackContext
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private fun removeWebRedemptionListener(callbackContext: CallbackContext) {
|
|
498
|
+
// The native listener stays registered; clearing the callback makes it a no-op.
|
|
499
|
+
releaseCallbackStream(webRedemptionCallback)
|
|
500
|
+
webRedemptionCallback = null
|
|
501
|
+
// Acknowledge the remove itself, so ITS callbackId is freed too. A void action that
|
|
502
|
+
// never answers leaks its own entry exactly like the listener's.
|
|
503
|
+
callbackContext.success()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Send one settled redemption to the JS listener.
|
|
508
|
+
*
|
|
509
|
+
* Everything the SDK hands over crosses here, so a test can fire a fabricated
|
|
510
|
+
* [PLYWebRedemptionResult] through the real delivery path and assert exactly what
|
|
511
|
+
* Cordova receives: the JSON body, and `keepCallback = true` so the stream stays open
|
|
512
|
+
* for the next redemption.
|
|
513
|
+
*
|
|
514
|
+
* A no-op when no listener is registered, which is why registering the SDK listener
|
|
515
|
+
* unconditionally at `start()` is behaviour-neutral.
|
|
516
|
+
*/
|
|
517
|
+
internal fun deliverRedemption(result: PLYWebRedemptionResult) {
|
|
518
|
+
val callback = webRedemptionCallback ?: return
|
|
519
|
+
val pluginResult = PluginResult(
|
|
520
|
+
PluginResult.Status.OK,
|
|
521
|
+
webRedemptionResultToJson(result, Companion::transformSubscriptionToMap)
|
|
522
|
+
)
|
|
523
|
+
pluginResult.keepCallback = true
|
|
524
|
+
callback.sendPluginResult(pluginResult)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* End a kept-alive Cordova callback stream, freeing its JavaScript closure.
|
|
529
|
+
*
|
|
530
|
+
* Every result the bridge sends a listener carries `keepCallback = true`, so dropping
|
|
531
|
+
* the native reference alone leaks the JS closure until the WebView reloads. NO_RESULT
|
|
532
|
+
* with `keepCallback = false` is cordova.js's own documented way out: it "is used to
|
|
533
|
+
* remove a callback from the list without calling the callbacks".
|
|
534
|
+
*/
|
|
535
|
+
private fun releaseCallbackStream(callback: CallbackContext?) {
|
|
536
|
+
if (callback == null) return
|
|
537
|
+
val terminal = PluginResult(PluginResult.Status.NO_RESULT)
|
|
538
|
+
terminal.keepCallback = false
|
|
539
|
+
callback.sendPluginResult(terminal)
|
|
540
|
+
}
|
|
541
|
+
|
|
395
542
|
private fun getAnonymousUserId(callbackContext: CallbackContext) {
|
|
396
543
|
callbackContext.success(Purchasely.anonymousUserId)
|
|
397
544
|
}
|
|
@@ -808,45 +955,10 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
808
955
|
}
|
|
809
956
|
}
|
|
810
957
|
|
|
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
|
-
|
|
833
958
|
private fun transformSubscriptionsToJson(list: List<PLYSubscriptionData>): JSONArray {
|
|
834
959
|
val result = JSONArray()
|
|
835
960
|
for (data in list) {
|
|
836
|
-
|
|
837
|
-
map["plan"] = transformPlanToMap(data.plan)
|
|
838
|
-
map["product"] = normalizeProductPlans(data.product.toMap())
|
|
839
|
-
map["subscriptionSource"] = when (data.data.storeType) {
|
|
840
|
-
StoreType.GOOGLE_PLAY_STORE -> StoreType.GOOGLE_PLAY_STORE.ordinal
|
|
841
|
-
StoreType.AMAZON_APP_STORE -> StoreType.AMAZON_APP_STORE.ordinal
|
|
842
|
-
StoreType.HUAWEI_APP_GALLERY -> StoreType.HUAWEI_APP_GALLERY.ordinal
|
|
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
|
|
847
|
-
else -> null
|
|
848
|
-
}
|
|
849
|
-
result.put(JSONObject(map))
|
|
961
|
+
result.put(JSONObject(transformSubscriptionToMap(data)))
|
|
850
962
|
}
|
|
851
963
|
return result
|
|
852
964
|
}
|
|
@@ -1374,6 +1486,7 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1374
1486
|
var defaultCallback: CallbackContext? = null
|
|
1375
1487
|
var eventsCallback: CallbackContext? = null
|
|
1376
1488
|
var attributesCallback: CallbackContext? = null
|
|
1489
|
+
var webRedemptionCallback: CallbackContext? = null
|
|
1377
1490
|
|
|
1378
1491
|
// Serializes a v6 PLYPresentationOutcome to the wire contract. `result` is kept as an
|
|
1379
1492
|
// int (PurchaseResult 0/1/2) for back-compat with the pre-6.0 JS layer.
|
|
@@ -1426,6 +1539,47 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1426
1539
|
)
|
|
1427
1540
|
}
|
|
1428
1541
|
|
|
1542
|
+
// These three live in the companion, not on the instance, so that
|
|
1543
|
+
// `Companion::transformSubscriptionToMap` is an UNBOUND reference. The SDK keeps
|
|
1544
|
+
// the redemption listener in a static (Purchasely.webRedemptionListener), so a
|
|
1545
|
+
// bound reference would keep the plugin, its Activity and its WebView reachable
|
|
1546
|
+
// for as long as that static holds the lambda. None of them touches instance state.
|
|
1547
|
+
// Hardening for the upcoming rc.4 native release: PLYPlan.toMap()'s raw "type" entry is
|
|
1548
|
+
// moving from an ordinal (Int) to the DistributionType name (String). transformPlanToMap
|
|
1549
|
+
// already overwrites "type" explicitly wherever it's used, but allProducts/
|
|
1550
|
+
// productWithIdentifier/the subscription's nested "product" field pass product.toMap()
|
|
1551
|
+
// straight through -- normalize those raw plan entries so the JS PlanType contract
|
|
1552
|
+
// (an ordinal) stays stable across both native formats. Both formats resolve to the same
|
|
1553
|
+
// ordinal since DistributionType's declared order already matches Purchasely.PlanType.
|
|
1554
|
+
private fun normalizePlanTypeOrdinal(raw: Any?): Int? = when (raw) {
|
|
1555
|
+
is Number -> raw.toInt()
|
|
1556
|
+
is String -> runCatching { DistributionType.valueOf(raw).ordinal }.getOrNull()
|
|
1557
|
+
else -> null
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
private fun normalizeProductPlans(map: Map<String, Any?>): Map<String, Any?> {
|
|
1561
|
+
val plans = map["plans"] as? List<*> ?: return map
|
|
1562
|
+
val normalized = plans.map { plan ->
|
|
1563
|
+
val planMap = plan as? Map<*, *> ?: return@map plan
|
|
1564
|
+
HashMap(planMap).apply { this["type"] = normalizePlanTypeOrdinal(this["type"]) }
|
|
1565
|
+
}
|
|
1566
|
+
return HashMap(map).apply { this["plans"] = normalized }
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Map one [PLYSubscriptionData] to the JS subscription shape.
|
|
1571
|
+
*
|
|
1572
|
+
* Shared by `userSubscriptions`, `userSubscriptionsHistory` and the web redemption
|
|
1573
|
+
* listener, whose `context.subscription` is the same type, so the three report one shape.
|
|
1574
|
+
*/
|
|
1575
|
+
internal fun transformSubscriptionToMap(data: PLYSubscriptionData): Map<String, Any?> {
|
|
1576
|
+
return HashMap(data.data.toMap()).apply {
|
|
1577
|
+
this["plan"] = transformPlanToMap(data.plan)
|
|
1578
|
+
this["product"] = normalizeProductPlans(data.product.toMap())
|
|
1579
|
+
this["subscriptionSource"] = subscriptionSourceFor(data.data.storeType)
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1429
1583
|
private fun transformPlanToMap(plan: PLYPlan?): Map<String?, Any?> {
|
|
1430
1584
|
if (plan == null) return HashMap()
|
|
1431
1585
|
val map = HashMap(plan.toMap())
|
|
@@ -1493,3 +1647,179 @@ class PurchaselyPlugin : CordovaPlugin(), CoroutineScope {
|
|
|
1493
1647
|
*/
|
|
1494
1648
|
}
|
|
1495
1649
|
}
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Map a native [StoreType] to the `subscriptionSource` wire value.
|
|
1653
|
+
*
|
|
1654
|
+
* The ordinal IS the wire value, and it matches iOS's `PLYSubscriptionSource` one for one:
|
|
1655
|
+
* apple 0, google 1, amazon 2, huawei 3, stripe 4, none 5. Verified against the shipped
|
|
1656
|
+
* 6.1.0 artifacts on both platforms, so there is no per-platform translation here.
|
|
1657
|
+
*
|
|
1658
|
+
* Listed exhaustively on purpose, and `internal` so a unit test drives THIS function
|
|
1659
|
+
* rather than re-deriving the mapping. The previous version named four stores and
|
|
1660
|
+
* collapsed `NONE` and `WEB_CHECKOUT_STRIPE` into a hardcoded 4, on the stated but FALSE
|
|
1661
|
+
* premise that iOS's `None` was 4. It is 5, and 4 is Stripe. So a Web2App subscription
|
|
1662
|
+
* reported `none`, and a sourceless one reported Stripe's value. An `else` branch is what
|
|
1663
|
+
* let that pass unnoticed; without one, a store type added by a future SDK fails the
|
|
1664
|
+
* Kotlin build here instead of silently reporting the wrong source.
|
|
1665
|
+
*/
|
|
1666
|
+
internal fun subscriptionSourceFor(storeType: StoreType?): Int? = when (storeType) {
|
|
1667
|
+
StoreType.APPLE_APP_STORE -> StoreType.APPLE_APP_STORE.ordinal
|
|
1668
|
+
StoreType.GOOGLE_PLAY_STORE -> StoreType.GOOGLE_PLAY_STORE.ordinal
|
|
1669
|
+
StoreType.AMAZON_APP_STORE -> StoreType.AMAZON_APP_STORE.ordinal
|
|
1670
|
+
StoreType.HUAWEI_APP_GALLERY -> StoreType.HUAWEI_APP_GALLERY.ordinal
|
|
1671
|
+
StoreType.WEB_CHECKOUT_STRIPE -> StoreType.WEB_CHECKOUT_STRIPE.ordinal
|
|
1672
|
+
StoreType.NONE -> StoreType.NONE.ordinal
|
|
1673
|
+
null -> null
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Apply the three 6.1.0 start options to a [Purchasely.Builder].
|
|
1678
|
+
*
|
|
1679
|
+
* Extracted so a unit test can assert WHICH builder call each resolver state produces. The
|
|
1680
|
+
* resolvers were covered on their own, but the branch consuming them was not: swapping the
|
|
1681
|
+
* `Clear` and `Set` cases, or inverting the override flag, passed every test in the
|
|
1682
|
+
* repository. That branch is the whole point of the release.
|
|
1683
|
+
*
|
|
1684
|
+
* `Absent` and `Invalid` make NO call, so the SDK keeps whatever it already has. `Clear`
|
|
1685
|
+
* calls `proxy(null)`, which is a real native operation and not the same as making no call.
|
|
1686
|
+
*/
|
|
1687
|
+
internal fun applyStartOptions(
|
|
1688
|
+
builder: Purchasely.Builder,
|
|
1689
|
+
proxyOption: PLYProxyOption,
|
|
1690
|
+
anonymousUserId: UUID?,
|
|
1691
|
+
anonymousUserIdOverride: Boolean,
|
|
1692
|
+
appHandlesRedemptionAlert: Boolean,
|
|
1693
|
+
redemptionListener: PLYWebRedemptionListener,
|
|
1694
|
+
) {
|
|
1695
|
+
when (proxyOption) {
|
|
1696
|
+
is PLYProxyOption.Absent -> {}
|
|
1697
|
+
is PLYProxyOption.Invalid -> {}
|
|
1698
|
+
is PLYProxyOption.Clear -> builder.proxy(null)
|
|
1699
|
+
is PLYProxyOption.Set -> builder.proxy(proxyOption.api)
|
|
1700
|
+
}
|
|
1701
|
+
anonymousUserId?.let { builder.anonymousUserId(it, anonymousUserIdOverride) }
|
|
1702
|
+
builder.webRedemptionListener(appHandlesRedemptionAlert, redemptionListener)
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* How the `proxy` start option resolves. Purchasely 6.1.0.
|
|
1707
|
+
*
|
|
1708
|
+
* The three JS states are not interchangeable, and a fourth case exists for a value that
|
|
1709
|
+
* will not convert to a URI. Collapsing [Absent] into [Clear] would turn every start into
|
|
1710
|
+
* an implicit clear; collapsing [Clear] into [Absent] would make a clear silently do
|
|
1711
|
+
* nothing.
|
|
1712
|
+
*/
|
|
1713
|
+
internal sealed class PLYProxyOption {
|
|
1714
|
+
/** The key is absent. Make no native call: leave the current setting untouched. */
|
|
1715
|
+
object Absent : PLYProxyOption()
|
|
1716
|
+
|
|
1717
|
+
/** The key is present and null. Call `proxy(null)` to clear the proxy. */
|
|
1718
|
+
object Clear : PLYProxyOption()
|
|
1719
|
+
|
|
1720
|
+
/** The key holds a usable value. Call `proxy(api)`. */
|
|
1721
|
+
data class Set(val api: String) : PLYProxyOption()
|
|
1722
|
+
|
|
1723
|
+
/** The key holds a value that will not convert. Log it and make no native call. */
|
|
1724
|
+
data class Invalid(val rawValue: String?) : PLYProxyOption()
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Resolve the `proxy` start option.
|
|
1729
|
+
*
|
|
1730
|
+
* Pure, and `internal` so a unit test drives the real bridge logic instead of a copy.
|
|
1731
|
+
*
|
|
1732
|
+
* `JSONObject.has` is what separates an absent key from an explicit null, and
|
|
1733
|
+
* `JSONObject.isNull` separates the null from a value. `optString` cannot do this on its
|
|
1734
|
+
* own: it renders `JSONObject.NULL` as the STRING `"null"`, which is exactly how a clear
|
|
1735
|
+
* used to be swallowed into the absent branch.
|
|
1736
|
+
*
|
|
1737
|
+
* The scheme, the host, and the absence of a query, a fragment and credentials are the
|
|
1738
|
+
* native SDK's business: it refuses a bad value with an error log and keeps the production
|
|
1739
|
+
* host, and it drops a trailing slash. This only rejects what will not convert at all,
|
|
1740
|
+
* which keeps the accepted set the same as the iOS bridge's `NSURL` conversion.
|
|
1741
|
+
*/
|
|
1742
|
+
internal fun resolveProxyOption(options: JSONObject): PLYProxyOption {
|
|
1743
|
+
if (!options.has("proxy")) return PLYProxyOption.Absent
|
|
1744
|
+
if (options.isNull("proxy")) return PLYProxyOption.Clear
|
|
1745
|
+
|
|
1746
|
+
val raw = options.opt("proxy")
|
|
1747
|
+
if (raw !is String) return PLYProxyOption.Invalid(raw?.toString())
|
|
1748
|
+
// A blank value is refused HERE, to match iOS: `[NSURL URLWithString:@""]` is nil, so
|
|
1749
|
+
// the iOS bridge resolves Invalid and never calls native. Leaving it to the SDK would
|
|
1750
|
+
// make the two platforms accept different sets of strings for the same option.
|
|
1751
|
+
if (raw.isBlank()) return PLYProxyOption.Invalid(raw)
|
|
1752
|
+
return try {
|
|
1753
|
+
URI(raw)
|
|
1754
|
+
PLYProxyOption.Set(raw)
|
|
1755
|
+
} catch (e: URISyntaxException) {
|
|
1756
|
+
PLYProxyOption.Invalid(raw)
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Flatten a [PLYWebRedemptionResult] to the 5-key shape the JS listener receives.
|
|
1762
|
+
* See `addWebRedemptionListener` in www/Purchasely.js for the shape itself.
|
|
1763
|
+
*
|
|
1764
|
+
* [subscriptionToMap] is injected so a unit test can drive this without constructing an SDK
|
|
1765
|
+
* [PLYSubscriptionData]. Production passes the plugin's own `transformSubscriptionToMap`.
|
|
1766
|
+
*
|
|
1767
|
+
* EVERY NULL IS PUT AS [JSONObject.NULL] EXPLICITLY, and the object is built here rather
|
|
1768
|
+
* than returned as a `Map` for the caller to wrap. `JSONObject(Map)` disagrees across
|
|
1769
|
+
* implementations: Android's wraps a null and keeps the key, the reference `org.json` DROPS
|
|
1770
|
+
* the entry — which would reach JS as `undefined` instead of `null`.
|
|
1771
|
+
*/
|
|
1772
|
+
internal fun webRedemptionResultToJson(
|
|
1773
|
+
result: PLYWebRedemptionResult,
|
|
1774
|
+
subscriptionToMap: (PLYSubscriptionData) -> Map<String, Any?>,
|
|
1775
|
+
): JSONObject {
|
|
1776
|
+
val json = JSONObject()
|
|
1777
|
+
when (result) {
|
|
1778
|
+
is PLYWebRedemptionResult.Success -> {
|
|
1779
|
+
json.put("isSuccess", true)
|
|
1780
|
+
val context = result.context
|
|
1781
|
+
if (context == null) {
|
|
1782
|
+
json.put("context", JSONObject.NULL)
|
|
1783
|
+
} else {
|
|
1784
|
+
val subscription = context.subscription
|
|
1785
|
+
json.put("context", JSONObject().put(
|
|
1786
|
+
"subscription",
|
|
1787
|
+
if (subscription == null) JSONObject.NULL
|
|
1788
|
+
else JSONObject(subscriptionToMap(subscription))
|
|
1789
|
+
))
|
|
1790
|
+
}
|
|
1791
|
+
json.put("replay", result.replay)
|
|
1792
|
+
json.put("errorCode", JSONObject.NULL)
|
|
1793
|
+
json.put("errorMessage", JSONObject.NULL)
|
|
1794
|
+
}
|
|
1795
|
+
is PLYWebRedemptionResult.Failure -> {
|
|
1796
|
+
json.put("isSuccess", false)
|
|
1797
|
+
json.put("context", JSONObject.NULL)
|
|
1798
|
+
// A failure still reports replay, so the shape never changes between branches.
|
|
1799
|
+
json.put("replay", false)
|
|
1800
|
+
json.put("errorCode", result.errorCode ?: JSONObject.NULL)
|
|
1801
|
+
json.put("errorMessage", result.errorMessage ?: JSONObject.NULL)
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return json
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* Parse a canonical UUID string, or return null.
|
|
1809
|
+
*
|
|
1810
|
+
* JS has no UUID type, so an anonymous user id crosses the bridge as a string.
|
|
1811
|
+
* `UUID.fromString` is lenient and accepts a short form such as `"1-2-3-4-5"` that the iOS
|
|
1812
|
+
* `NSUUID` parser refuses. The round-trip check makes both platforms agree on what
|
|
1813
|
+
* "canonical" means, so one id string is accepted, or refused, on both.
|
|
1814
|
+
*
|
|
1815
|
+
* `internal` so a unit test drives it without an Android logger. The caller logs a refusal.
|
|
1816
|
+
*/
|
|
1817
|
+
internal fun parseCanonicalUuid(value: String?): UUID? {
|
|
1818
|
+
if (value == null) return null
|
|
1819
|
+
val parsed = try {
|
|
1820
|
+
UUID.fromString(value)
|
|
1821
|
+
} catch (e: IllegalArgumentException) {
|
|
1822
|
+
return null
|
|
1823
|
+
}
|
|
1824
|
+
return if (parsed.toString().equals(value, ignoreCase = true)) parsed else null
|
|
1825
|
+
}
|
|
@@ -27,6 +27,40 @@
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/// `PLYWebRedemptionDelegate`. The SDK calls this on the main thread, once per settled
|
|
31
|
+
/// redemption, on success and on failure alike, and always after the matching
|
|
32
|
+
/// REDEMPTION_CONSUMED / REDEMPTION_FAILED event reached the event delegate.
|
|
33
|
+
///
|
|
34
|
+
/// Mapped to the flat 5-key shape the Android bridge emits, so one JS listener drives both
|
|
35
|
+
/// platforms. `context` and `context.subscription` stay separately nullable: a success can
|
|
36
|
+
/// carry no context at all, and a present context can carry no subscription.
|
|
37
|
+
///
|
|
38
|
+
/// `errorMessage` can hold the backend's masked email hint for an expired link. The
|
|
39
|
+
/// REDEMPTION_FAILED event drops that hint on purpose; this channel keeps it, so the app
|
|
40
|
+
/// can tell the user where the fresh link went.
|
|
41
|
+
///
|
|
42
|
+
/// The Android bridge carries it too: `RedemptionOutcome.Expired.toResult()` appends the
|
|
43
|
+
/// same hint. So the "show it, never log it" rule the JS docs state is unconditional, and
|
|
44
|
+
/// must not be written as an iOS-only caveat.
|
|
45
|
+
- (void)webRedemptionCompletedWithResult:(PLYWebRedemptionResult * _Nonnull)result {
|
|
46
|
+
if (self.webRedemptionCommand == nil) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
PLYSubscription *subscription = result.context.subscription;
|
|
51
|
+
NSDictionary<NSString *, id> *body =
|
|
52
|
+
[CDVPurchasely webRedemptionBodyWithSuccess:result.isSuccess
|
|
53
|
+
hasContext:result.context != nil
|
|
54
|
+
subscription:subscription != nil ? subscription.asDictionary : nil
|
|
55
|
+
replay:result.replay
|
|
56
|
+
errorCode:result.errorCode
|
|
57
|
+
errorMessage:result.errorMessage];
|
|
58
|
+
|
|
59
|
+
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:body];
|
|
60
|
+
[pluginResult setKeepCallbackAsBool:YES];
|
|
61
|
+
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.webRedemptionCommand.callbackId];
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
- (void)reloadContent: (NSNotification *)aNotification {
|
|
31
65
|
if (self.purchasedCommand) {
|
|
32
66
|
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
|
package/src/ios/CDVPurchasely.h
CHANGED
|
@@ -8,11 +8,59 @@
|
|
|
8
8
|
#import <Cordova/CDVPlugin.h>
|
|
9
9
|
#import <Purchasely/Purchasely-Swift.h>
|
|
10
10
|
|
|
11
|
+
/// How the `proxy` start option resolves. Purchasely 6.1.0.
|
|
12
|
+
///
|
|
13
|
+
/// The three JS states are NOT interchangeable, and a fourth case exists for a value the
|
|
14
|
+
/// bridge cannot convert. `proxyWithApi:` takes an `NSURL *_Nullable`, where nil means
|
|
15
|
+
/// CLEAR, so an unconvertible string must skip the modifier rather than pass nil: passing
|
|
16
|
+
/// nil would silently disable a proxy the app explicitly asked for, because of a typo.
|
|
17
|
+
typedef NS_ENUM(NSInteger, CDVPurchaselyProxyOption) {
|
|
18
|
+
/// The key is absent. Make no native call: leave the current setting untouched.
|
|
19
|
+
CDVPurchaselyProxyOptionAbsent = 0,
|
|
20
|
+
/// The key is present and null. Call `proxyWithApi:nil` to clear the proxy.
|
|
21
|
+
CDVPurchaselyProxyOptionClear,
|
|
22
|
+
/// The key holds a convertible string. Call `proxyWithApi:` with the URL.
|
|
23
|
+
CDVPurchaselyProxyOptionSet,
|
|
24
|
+
/// The key holds a string `NSURL` cannot convert. Log and make no native call.
|
|
25
|
+
CDVPurchaselyProxyOptionInvalid
|
|
26
|
+
};
|
|
27
|
+
|
|
11
28
|
// Protocol conformance (PLYEventDelegate / PLYUserAttributeDelegate) is declared on the
|
|
12
29
|
// CDVPurchasely (Events) and (UserAttributes) categories, which implement the delegate methods.
|
|
13
30
|
@interface CDVPurchasely : CDVPlugin {
|
|
14
31
|
}
|
|
15
32
|
|
|
33
|
+
/// Resolve the `proxy` start option to one of the four cases above.
|
|
34
|
+
///
|
|
35
|
+
/// Pure, and exposed so a unit test drives the real bridge logic instead of a copy. `value`
|
|
36
|
+
/// is the raw option, so `nil` for an absent key and `NSNull` for an explicit JS null.
|
|
37
|
+
/// `outUrl` receives the URL only for `CDVPurchaselyProxyOptionSet`.
|
|
38
|
+
+ (CDVPurchaselyProxyOption)proxyOptionFor:(id _Nullable)value url:(NSURL * _Nullable * _Nullable)outUrl;
|
|
39
|
+
|
|
40
|
+
/// Build the flat 5-key body a settled Web2App redemption reports to JS.
|
|
41
|
+
///
|
|
42
|
+
/// Takes primitives rather than a `PLYWebRedemptionResult`, because that class declares
|
|
43
|
+
/// `init` unavailable and a test cannot construct one. `hasContext` is separate from
|
|
44
|
+
/// `subscription` on purpose: a present context carrying no subscription is NOT the same as
|
|
45
|
+
/// no context at all, and both must stay expressible.
|
|
46
|
+
///
|
|
47
|
+
/// Exposed so the XCTest target asserts the real shape the delegate emits, rather than a
|
|
48
|
+
/// copy of it. Matches the React Native bridge's seam of the same name.
|
|
49
|
+
+ (NSDictionary<NSString *, id> * _Nonnull)webRedemptionBodyWithSuccess:(BOOL)isSuccess
|
|
50
|
+
hasContext:(BOOL)hasContext
|
|
51
|
+
subscription:(NSDictionary * _Nullable)subscription
|
|
52
|
+
replay:(BOOL)replay
|
|
53
|
+
errorCode:(NSString * _Nullable)errorCode
|
|
54
|
+
errorMessage:(NSString * _Nullable)errorMessage;
|
|
55
|
+
|
|
56
|
+
/// Parse a canonical UUID string, or return nil.
|
|
57
|
+
///
|
|
58
|
+
/// JS has no UUID type, so an anonymous user id crosses the bridge as a string. Exposed so
|
|
59
|
+
/// a unit test can pin the cross-platform contract: this refuses the lenient short form
|
|
60
|
+
/// (`"1-2-3-4-5"`) that Android's `UUID.fromString` accepts, which is why the Android
|
|
61
|
+
/// bridge adds a round-trip check.
|
|
62
|
+
+ (NSUUID * _Nullable)canonicalUUIDFromString:(id _Nullable)value;
|
|
63
|
+
|
|
16
64
|
// The presentation currently displayed (v6 uses id<PLYPresentation> for close()/back()).
|
|
17
65
|
@property (nonatomic, strong) id<PLYPresentation> currentPresentation;
|
|
18
66
|
|
|
@@ -20,6 +68,12 @@
|
|
|
20
68
|
@property CDVInvokedUrlCommand* eventCommand;
|
|
21
69
|
@property CDVInvokedUrlCommand* attributeCommand;
|
|
22
70
|
|
|
71
|
+
// Purchasely 6.1.0. The command `addWebRedemptionListener` recorded, or nil. The
|
|
72
|
+
// PLYWebRedemptionDelegate is registered on the builder chain in `start:` (the native SDK
|
|
73
|
+
// has no runtime setter), so this is the only switch: nil makes
|
|
74
|
+
// `webRedemptionCompletedWithResult:` a no-op.
|
|
75
|
+
@property CDVInvokedUrlCommand* webRedemptionCommand;
|
|
76
|
+
|
|
23
77
|
@property (nonatomic) NSMutableArray<id<PLYPresentation>> *presentationsLoaded;
|
|
24
78
|
|
|
25
79
|
@property (nonatomic) CDVInvokedUrlCommand* purchaseResolve;
|
|
@@ -57,6 +111,9 @@
|
|
|
57
111
|
- (void)userSubscriptionsHistory:(CDVInvokedUrlCommand*)command;
|
|
58
112
|
- (void)addEventsListener:(CDVInvokedUrlCommand*)command;
|
|
59
113
|
- (void)removeEventsListener:(CDVInvokedUrlCommand*)command;
|
|
114
|
+
- (void)releaseCallbackStream:(CDVInvokedUrlCommand * _Nullable)command;
|
|
115
|
+
- (void)addWebRedemptionListener:(CDVInvokedUrlCommand*)command;
|
|
116
|
+
- (void)removeWebRedemptionListener:(CDVInvokedUrlCommand*)command;
|
|
60
117
|
- (void)registerActionInterceptor:(CDVInvokedUrlCommand*)command;
|
|
61
118
|
- (void)unregisterActionInterceptor:(CDVInvokedUrlCommand*)command;
|
|
62
119
|
- (void)completeActionInterceptor:(CDVInvokedUrlCommand*)command;
|
package/src/ios/CDVPurchasely.m
CHANGED
|
@@ -29,6 +29,82 @@
|
|
|
29
29
|
self.pendingInterceptCompletions = [NSMutableDictionary new];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
+ (CDVPurchaselyProxyOption)proxyOptionFor:(id _Nullable)value url:(NSURL * _Nullable * _Nullable)outUrl {
|
|
33
|
+
if (outUrl != NULL) {
|
|
34
|
+
*outUrl = nil;
|
|
35
|
+
}
|
|
36
|
+
// An absent key and an explicit null are different operations. Check NSNull FIRST:
|
|
37
|
+
// it is a real object, so an `isKindOfClass:[NSString class]` test would fall through
|
|
38
|
+
// to the absent branch and turn a requested clear into a silent no-op.
|
|
39
|
+
if (value == nil) {
|
|
40
|
+
return CDVPurchaselyProxyOptionAbsent;
|
|
41
|
+
}
|
|
42
|
+
if (value == [NSNull null]) {
|
|
43
|
+
return CDVPurchaselyProxyOptionClear;
|
|
44
|
+
}
|
|
45
|
+
if (![value isKindOfClass:[NSString class]]) {
|
|
46
|
+
return CDVPurchaselyProxyOptionInvalid;
|
|
47
|
+
}
|
|
48
|
+
// Do not validate the scheme, the host, a query, a fragment or credentials here. The
|
|
49
|
+
// native SDK refuses those with an error log and keeps the production host, and it
|
|
50
|
+
// drops a trailing slash. The bridge only rejects what will not convert at all.
|
|
51
|
+
NSURL *url = [NSURL URLWithString:(NSString *)value];
|
|
52
|
+
if (url == nil) {
|
|
53
|
+
return CDVPurchaselyProxyOptionInvalid;
|
|
54
|
+
}
|
|
55
|
+
if (outUrl != NULL) {
|
|
56
|
+
*outUrl = url;
|
|
57
|
+
}
|
|
58
|
+
return CDVPurchaselyProxyOptionSet;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
+ (NSDictionary<NSString *, id> * _Nonnull)webRedemptionBodyWithSuccess:(BOOL)isSuccess
|
|
62
|
+
hasContext:(BOOL)hasContext
|
|
63
|
+
subscription:(NSDictionary * _Nullable)subscription
|
|
64
|
+
replay:(BOOL)replay
|
|
65
|
+
errorCode:(NSString * _Nullable)errorCode
|
|
66
|
+
errorMessage:(NSString * _Nullable)errorMessage {
|
|
67
|
+
// Every key is always present, on both branches, so a JS listener reads one shape
|
|
68
|
+
// whether the redemption was granted or refused. Absence is NSNull, never a missing
|
|
69
|
+
// key: a missing key reaches JS as `undefined` instead of `null`.
|
|
70
|
+
id context = [NSNull null];
|
|
71
|
+
if (hasContext) {
|
|
72
|
+
context = @{ @"subscription": subscription ?: [NSNull null] };
|
|
73
|
+
}
|
|
74
|
+
return @{
|
|
75
|
+
@"isSuccess": @(isSuccess),
|
|
76
|
+
@"context": context,
|
|
77
|
+
@"replay": @(replay),
|
|
78
|
+
@"errorCode": errorCode ?: [NSNull null],
|
|
79
|
+
@"errorMessage": errorMessage ?: [NSNull null]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
+ (NSUUID * _Nullable)canonicalUUIDFromString:(id _Nullable)value {
|
|
84
|
+
if (![value isKindOfClass:[NSString class]]) {
|
|
85
|
+
return nil;
|
|
86
|
+
}
|
|
87
|
+
return [[NSUUID alloc] initWithUUIDString:(NSString *)value];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Cordova calls this when the WebView navigates, which invalidates every callbackId the
|
|
91
|
+
/// previous page handed us. Without it the stored commands stay live and every listener
|
|
92
|
+
/// callback is sent to a dead callbackId after a reload.
|
|
93
|
+
///
|
|
94
|
+
/// The redemption case is the one that matters most: `webRedemptionDelegate:` is set on the
|
|
95
|
+
/// builder at `start:`, and the SDK holds the delegate weakly, so this object keeps
|
|
96
|
+
/// receiving outcomes for as long as the plugin lives.
|
|
97
|
+
/// `webRedemptionCompletedWithResult:` reads `webRedemptionCommand` at fire time, so a
|
|
98
|
+
/// reloaded page can re-register and keep working; clearing here makes the window in
|
|
99
|
+
/// between a clean no-op rather than a send on a dead callbackId.
|
|
100
|
+
- (void)onReset {
|
|
101
|
+
self.eventCommand = nil;
|
|
102
|
+
self.attributeCommand = nil;
|
|
103
|
+
self.webRedemptionCommand = nil;
|
|
104
|
+
self.purchasedCommand = nil;
|
|
105
|
+
[super onReset];
|
|
106
|
+
}
|
|
107
|
+
|
|
32
108
|
- (void)start:(CDVInvokedUrlCommand*)command {
|
|
33
109
|
// v6: a single options dictionary (see the JS↔native contract), no longer positional args.
|
|
34
110
|
NSDictionary *opts = [command argumentAtIndex:0];
|
|
@@ -91,6 +167,68 @@
|
|
|
91
167
|
builder = [builder allowCampaigns:allowCampaigns.boolValue];
|
|
92
168
|
}
|
|
93
169
|
|
|
170
|
+
// Both bridges report a refused-and-skipped option at the SAME severity, and
|
|
171
|
+
// deliberately with a plain log line on both: NSLog here, Log.e on Android. Neither
|
|
172
|
+
// renders UI. Do not reach for anything that puts an overlay or an alert in front of
|
|
173
|
+
// the host app: start() continues, the option was simply ignored, and a third-party
|
|
174
|
+
// SDK has no business interrupting someone else's app over an option it chose to skip.
|
|
175
|
+
|
|
176
|
+
// v6.1.0: JS has no UUID type, so the id crosses the bridge as a string and is parsed
|
|
177
|
+
// here. The native builder takes an NSUUID, which is where the guarantee used to live;
|
|
178
|
+
// a string-typed bridge is the only place left to catch a bad value. Refuse it loudly
|
|
179
|
+
// and skip the option. The SDK still starts, matching how native treats an unusable
|
|
180
|
+
// proxy url.
|
|
181
|
+
id anonymousUserId = opts[@"anonymousUserId"];
|
|
182
|
+
if ([anonymousUserId isKindOfClass:[NSString class]]) {
|
|
183
|
+
NSUUID *parsed = [CDVPurchasely canonicalUUIDFromString:anonymousUserId];
|
|
184
|
+
if (parsed == nil) {
|
|
185
|
+
// The value is NOT logged. A mis-wired field lands here just as easily as a
|
|
186
|
+
// typo -- an email, an appUserId -- and a device log is captured during
|
|
187
|
+
// support. The length is enough to tell a truncated id from a wrong field.
|
|
188
|
+
NSLog(@"[Purchasely] `anonymousUserId` must be a canonical UUID string, for example "
|
|
189
|
+
"\"3f2504e0-4f89-11d3-9a0c-0305e82c3301\". Received a %lu-character value. "
|
|
190
|
+
"The anonymous user id is not applied.",
|
|
191
|
+
(unsigned long)((NSString *)anonymousUserId).length);
|
|
192
|
+
} else {
|
|
193
|
+
NSNumber *override = opts[@"anonymousUserIdOverride"];
|
|
194
|
+
BOOL shouldOverride = [override isKindOfClass:[NSNumber class]] ? override.boolValue : NO;
|
|
195
|
+
builder = [builder appAnonymousUserId:parsed override:shouldOverride];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// v6.1.0: three states, and they are not interchangeable. An absent key makes no
|
|
200
|
+
// native call and leaves the current setting untouched; an explicit null clears the
|
|
201
|
+
// proxy and returns to api.purchasely.io, which is a supported operation and not an
|
|
202
|
+
// error; a string routes the API host. A value NSURL cannot convert skips the
|
|
203
|
+
// modifier, because `proxyWithApi:nil` means CLEAR, not "ignore this value", so
|
|
204
|
+
// passing nil would silently disable a proxy the app asked for.
|
|
205
|
+
NSURL *proxyUrl = nil;
|
|
206
|
+
switch ([CDVPurchasely proxyOptionFor:opts[@"proxy"] url:&proxyUrl]) {
|
|
207
|
+
case CDVPurchaselyProxyOptionAbsent:
|
|
208
|
+
break;
|
|
209
|
+
case CDVPurchaselyProxyOptionClear:
|
|
210
|
+
builder = [builder proxyWithApi:nil];
|
|
211
|
+
break;
|
|
212
|
+
case CDVPurchaselyProxyOptionSet:
|
|
213
|
+
builder = [builder proxyWithApi:proxyUrl];
|
|
214
|
+
break;
|
|
215
|
+
case CDVPurchaselyProxyOptionInvalid:
|
|
216
|
+
NSLog(@"[Purchasely] `proxy` must be an https base URL, for example "
|
|
217
|
+
"\"https://svc.purchasely.io\", or null to clear the proxy. Received "
|
|
218
|
+
"\"%@\". The proxy is not applied.", opts[@"proxy"]);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// v6.1.0: registered unconditionally. The native SDK has no runtime setter on purpose,
|
|
223
|
+
// because a redemption can settle during `start()` (a cold start that the `ply/redeem`
|
|
224
|
+
// link itself triggered, or a token a previous launch left pending). The delegate
|
|
225
|
+
// callback returns early when `addWebRedemptionListener` recorded no command, so this
|
|
226
|
+
// is behaviour-neutral by default.
|
|
227
|
+
NSNumber *handlesRedemptionAlert = opts[@"appHandlesRedemptionAlert"];
|
|
228
|
+
builder = [builder webRedemptionDelegate:self
|
|
229
|
+
appHandlesRedemptionAlert:[handlesRedemptionAlert isKindOfClass:[NSNumber class]]
|
|
230
|
+
? handlesRedemptionAlert.boolValue : NO];
|
|
231
|
+
|
|
94
232
|
// Cold-start deeplink URL captured at launch (handled automatically once start completes).
|
|
95
233
|
NSString *deeplink = opts[@"deeplink"];
|
|
96
234
|
if ([deeplink isKindOfClass:[NSString class]] && deeplink.length > 0) {
|
|
@@ -527,6 +665,41 @@
|
|
|
527
665
|
self.eventCommand = nil;
|
|
528
666
|
}
|
|
529
667
|
|
|
668
|
+
/// End a kept-alive Cordova callback stream, freeing its JavaScript closure.
|
|
669
|
+
///
|
|
670
|
+
/// Every result this bridge sends a listener carries `keepCallback:YES`, so dropping the
|
|
671
|
+
/// native command alone leaks the JS closure until the WebView reloads. NO_RESULT with
|
|
672
|
+
/// `keepCallback:NO` is cordova.js's own documented way out: it "is used to remove a
|
|
673
|
+
/// callback from the list without calling the callbacks".
|
|
674
|
+
- (void)releaseCallbackStream:(CDVInvokedUrlCommand * _Nullable)command {
|
|
675
|
+
if (command == nil) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
CDVPluginResult *terminal = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT];
|
|
679
|
+
[terminal setKeepCallbackAsBool:NO];
|
|
680
|
+
[self.commandDelegate sendPluginResult:terminal callbackId:command.callbackId];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// v6.1.0. The PLYWebRedemptionDelegate is registered on the builder chain in `start:` (the
|
|
684
|
+
// native SDK has no runtime setter, because a redemption can settle during start()), so
|
|
685
|
+
// this action only records the command to route the outcome to. Call it BEFORE start().
|
|
686
|
+
- (void)addWebRedemptionListener:(CDVInvokedUrlCommand*)command {
|
|
687
|
+
// Close the previous stream before replacing it, or its JS closure stays in
|
|
688
|
+
// cordova.callbacks forever.
|
|
689
|
+
[self releaseCallbackStream:self.webRedemptionCommand];
|
|
690
|
+
self.webRedemptionCommand = command;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
- (void)removeWebRedemptionListener:(CDVInvokedUrlCommand*)command {
|
|
694
|
+
// The delegate stays registered. Clearing the command makes
|
|
695
|
+
// `webRedemptionCompletedWithResult:` a no-op.
|
|
696
|
+
[self releaseCallbackStream:self.webRedemptionCommand];
|
|
697
|
+
self.webRedemptionCommand = nil;
|
|
698
|
+
// Acknowledge the remove itself, so ITS callbackId is freed too. A void action that
|
|
699
|
+
// never answers leaks its own entry exactly like the listener's.
|
|
700
|
+
[self successFor:command resultBool:YES];
|
|
701
|
+
}
|
|
702
|
+
|
|
530
703
|
- (void)removeUserAttributeListener:(CDVInvokedUrlCommand*)command {
|
|
531
704
|
// v6 `setUserAttributeDelegate:` is _Nonnull (no native unregister). Clearing
|
|
532
705
|
// attributeCommand makes the user-attribute callbacks a no-op.
|
package/www/Purchasely.js
CHANGED
|
@@ -59,11 +59,25 @@ function presentationDispatcher(success, callbacks) {
|
|
|
59
59
|
// allowDeeplink (bool, optional)
|
|
60
60
|
// allowCampaigns (bool, optional)
|
|
61
61
|
// deeplink (string, optional — cold-start deeplink URL)
|
|
62
|
+
//
|
|
63
|
+
// Purchasely 6.1.0 adds four options:
|
|
64
|
+
// anonymousUserId (string, optional — a canonical UUID string; a bad value is
|
|
65
|
+
// logged and skipped, start() still succeeds)
|
|
66
|
+
// anonymousUserIdOverride (bool, optional — false; true SPLITS the user history)
|
|
67
|
+
// proxy (string|null, optional — Android+iOS. THREE STATES:
|
|
68
|
+
// 'https://…' routes the API host
|
|
69
|
+
// null CLEARS it, back to api.purchasely.io
|
|
70
|
+
// key absent leaves the current setting untouched
|
|
71
|
+
// A clear is a supported native operation, not an error.)
|
|
72
|
+
// appHandlesRedemptionAlert (bool, optional — false keeps the SDK popin, true hands the
|
|
73
|
+
// result screen to the app. See addWebRedemptionListener.)
|
|
74
|
+
//
|
|
75
|
+
// README.md "What is new in 6.1.0" is the reference for all four.
|
|
62
76
|
exports.start = function (options, success, error) {
|
|
63
77
|
var opts = options || {};
|
|
64
78
|
var cordovaSdkVersion = cordova.define.moduleMap['cordova/plugin_list'].exports['metadata']['cordova-plugin-purchasely']
|
|
65
79
|
if(!cordovaSdkVersion) {
|
|
66
|
-
cordovaSdkVersion = "6.
|
|
80
|
+
cordovaSdkVersion = "6.1.1";
|
|
67
81
|
}
|
|
68
82
|
opts.sdkVersion = cordovaSdkVersion;
|
|
69
83
|
exec(success, error, 'Purchasely', 'start', [opts]);
|
|
@@ -89,7 +103,76 @@ PLYStartBuilder.prototype.storekitVersion = function (value) { this._options.sto
|
|
|
89
103
|
PLYStartBuilder.prototype.storeKit1 = function (value) { this._options.storeKit1 = value; return this; };
|
|
90
104
|
PLYStartBuilder.prototype.deeplink = function (value) { this._options.deeplink = value; return this; };
|
|
91
105
|
|
|
106
|
+
// Purchasely 6.1.0. `id` must be a canonical UUID string; `override` defaults to false.
|
|
107
|
+
// See the exports.start option block for the full contract.
|
|
108
|
+
PLYStartBuilder.prototype.anonymousUserId = function (id, override) {
|
|
109
|
+
this._options.anonymousUserId = id;
|
|
110
|
+
this._options.anonymousUserIdOverride = override === undefined ? false : override;
|
|
111
|
+
return this;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Purchasely 6.1.0. Pass an https base URL to route the API host, or null to CLEAR a
|
|
115
|
+
// proxy and return to api.purchasely.io. Both differ from never calling the modifier,
|
|
116
|
+
// which leaves the current setting untouched. See the exports.start option block.
|
|
117
|
+
//
|
|
118
|
+
// An argument is required. `proxy()` with none is refused, because the no-argument native
|
|
119
|
+
// modifiers disagree across platforms: iOS `proxy()` routes through Purchasely's own
|
|
120
|
+
// proxy at svc.purchasely.io, while Android `proxy()` clears. A Cordova shorthand would
|
|
121
|
+
// therefore mean two different things on the two platforms.
|
|
122
|
+
//
|
|
123
|
+
// `undefined` is never stored: JSON.stringify drops an undefined-valued key, which would
|
|
124
|
+
// make an explicit clear indistinguishable from an absent option on both natives.
|
|
125
|
+
PLYStartBuilder.prototype.proxy = function (api) {
|
|
126
|
+
// An explicit `undefined` is treated exactly like no argument at all, and NOT as null.
|
|
127
|
+
// The two public entry points have to agree on the same input: JSON.stringify drops an
|
|
128
|
+
// undefined-valued key, so `start({ proxy: undefined })` reaches native as Absent.
|
|
129
|
+
// Mapping it to null here would make `builder(k).proxy(config.proxy)` CLEAR the proxy
|
|
130
|
+
// whenever config.proxy has not loaded yet, which is the opposite of leaving it alone.
|
|
131
|
+
if (arguments.length === 0 || api === undefined) {
|
|
132
|
+
defaultError('[Purchasely] proxy() requires an argument: an https base URL, or ' +
|
|
133
|
+
'null to clear the proxy. The proxy option is not applied.');
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
this._options.proxy = api;
|
|
137
|
+
return this;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Purchasely 6.1.0. false (the default) keeps the SDK's own redemption popin.
|
|
141
|
+
PLYStartBuilder.prototype.appHandlesRedemptionAlert = function (handles) {
|
|
142
|
+
this._options.appHandlesRedemptionAlert = handles;
|
|
143
|
+
return this;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Purchasely 6.1.0: the PRIMARY way to receive Web2App redemption outcomes.
|
|
147
|
+
//
|
|
148
|
+
// Purchasely.builder(apiKey).webRedemptionListener(onRedemption, true).start()
|
|
149
|
+
//
|
|
150
|
+
// The callback never crosses the bridge: each native registers ITSELF as the delegate at
|
|
151
|
+
// start() and forwards outcomes as a Cordova callback stream, so this is a JS concern only.
|
|
152
|
+
//
|
|
153
|
+
// STORED HERE, SUBSCRIBED IN start(), just before the native call. Do not move it back:
|
|
154
|
+
// subscribing at chain time leaks a live callback from a builder that is never started. A
|
|
155
|
+
// redemption can only settle once the SDK runs, so subscribing here still covers one that
|
|
156
|
+
// settles DURING start(), which is the case the feature exists for.
|
|
157
|
+
//
|
|
158
|
+
// 2nd argument = appHandlesRedemptionAlert; omitting it keeps the native default. Callback
|
|
159
|
+
// first, matching iOS -- the natives disagree on order (Android takes the flag first).
|
|
160
|
+
PLYStartBuilder.prototype.webRedemptionListener = function (callback, appHandlesRedemptionAlert) {
|
|
161
|
+
// Kept off _options on purpose: that object is the exec payload, and a callback has no
|
|
162
|
+
// business being serialized into it.
|
|
163
|
+
this._webRedemptionCallback = callback;
|
|
164
|
+
if (appHandlesRedemptionAlert !== undefined) {
|
|
165
|
+
this._options.appHandlesRedemptionAlert = appHandlesRedemptionAlert;
|
|
166
|
+
}
|
|
167
|
+
return this;
|
|
168
|
+
};
|
|
169
|
+
|
|
92
170
|
PLYStartBuilder.prototype.start = function (success, error) {
|
|
171
|
+
// Subscribe immediately before the native start call, never earlier. See
|
|
172
|
+
// webRedemptionListener above for why this is deferred to here.
|
|
173
|
+
if (this._webRedemptionCallback) {
|
|
174
|
+
exports.addWebRedemptionListener(this._webRedemptionCallback);
|
|
175
|
+
}
|
|
93
176
|
if (success) {
|
|
94
177
|
exports.start(this._options, success, error);
|
|
95
178
|
return undefined;
|
|
@@ -118,6 +201,37 @@ exports.addEventsListener = function (success, error) {
|
|
|
118
201
|
exec(success, error, 'Purchasely', 'addEventsListener', []);
|
|
119
202
|
};
|
|
120
203
|
|
|
204
|
+
// Purchasely 6.1.0: the outcome of a Web2App redemption ({scheme}://ply/redeem/{token}).
|
|
205
|
+
//
|
|
206
|
+
// SECONDARY PATH, for replacing the listener while the SDK already runs. Prefer
|
|
207
|
+
// builder(apiKey).webRedemptionListener(cb). Both share ONE native slot: last caller wins.
|
|
208
|
+
//
|
|
209
|
+
// success receives { isSuccess, context, replay, errorCode, errorMessage }:
|
|
210
|
+
// isSuccess Bool.
|
|
211
|
+
// context { subscription } or null, and `subscription` is separately nullable.
|
|
212
|
+
// Same shape as userSubscriptions(), so purchaseToken, nextRenewalDate and
|
|
213
|
+
// cancelledDate may be absent -- Android sends an explicit null, iOS omits
|
|
214
|
+
// the key. A truthiness check covers both; `!== undefined` does not.
|
|
215
|
+
// replay Bool. The SERVER says the token was redeemed before. False on failure.
|
|
216
|
+
// errorCode 'EXPIRED_REDEMPTION_TOKEN' | 'INVALID_REDEMPTION_TOKEN' | null.
|
|
217
|
+
// errorMessage Human-readable, English, or null. Never contains the token.
|
|
218
|
+
//
|
|
219
|
+
// Called on the main thread, exactly once per settled redemption.
|
|
220
|
+
//
|
|
221
|
+
// PRIVACY, BOTH PLATFORMS: errorMessage for an expired link can carry a MASKED EMAIL
|
|
222
|
+
// ADDRESS. Show it to the user; never log it or send it to analytics or a crash reporter.
|
|
223
|
+
// The rule is unconditional -- do NOT gate it on a platform check. The REDEMPTION_FAILED
|
|
224
|
+
// event drops the hint, so that channel is safe.
|
|
225
|
+
exports.addWebRedemptionListener = function (success, error) {
|
|
226
|
+
exec(success, error, 'Purchasely', 'addWebRedemptionListener', []);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Purchasely 6.1.0: stop receiving redemption outcomes. The native delegate stays
|
|
230
|
+
// registered (it is fixed at start()); clearing the callback makes it a no-op.
|
|
231
|
+
exports.removeWebRedemptionListener = function () {
|
|
232
|
+
exec(() => {}, defaultError, 'Purchasely', 'removeWebRedemptionListener', []);
|
|
233
|
+
};
|
|
234
|
+
|
|
121
235
|
exports.addUserAttributeListener = function(success, error) {
|
|
122
236
|
exec(success, error, 'Purchasely', 'addUserAttributeListener', []);
|
|
123
237
|
};
|
|
@@ -508,6 +622,11 @@ exports.userDidConsumeSubscriptionContent = function () {
|
|
|
508
622
|
|
|
509
623
|
// PAR-29: invalidateCache forces a fresh fetch instead of returning the cached list
|
|
510
624
|
// (native default false on both platforms).
|
|
625
|
+
// A subscription's purchaseToken, nextRenewalDate and cancelledDate can all be ABSENT, and
|
|
626
|
+
// the two platforms report absence differently: Android sends the key with an explicit
|
|
627
|
+
// null, iOS omits it entirely (and never emits purchaseToken at all, because the native
|
|
628
|
+
// PLYSubscription has no such property). Handle both -- a truthiness check covers them,
|
|
629
|
+
// `!== undefined` does not. Same shape as the web redemption context's `subscription`.
|
|
511
630
|
exports.userSubscriptions = function (success, error, invalidateCache) {
|
|
512
631
|
exec(success, defaultError, 'Purchasely', 'userSubscriptions', [!!invalidateCache]);
|
|
513
632
|
};
|
|
@@ -731,12 +850,23 @@ exports.PurchaseResult = {
|
|
|
731
850
|
RESTORED: 2
|
|
732
851
|
}
|
|
733
852
|
|
|
853
|
+
// Values are the NATIVE raw values, verified against the shipped 6.1.0 artifacts:
|
|
854
|
+
// iOS PLYSubscriptionSource (stripe = 4, none = 5) and Android StoreType ordinals
|
|
855
|
+
// (WEB_CHECKOUT_STRIPE = 4, NONE = 5). Both platforms agree, so there is no
|
|
856
|
+
// per-platform mapping here and no renumbering hazard.
|
|
857
|
+
//
|
|
858
|
+
// `webCheckoutStripe` was missing and `none` was 4, which was correct before the native
|
|
859
|
+
// SDKs inserted the Stripe case at 4 (Android ~5.5.0) and pushed NONE to 5. A Web2App
|
|
860
|
+
// subscription therefore reported `none`, and a genuinely sourceless one reported a value
|
|
861
|
+
// this object had no name for. A Web2App redemption grants a subscription from exactly
|
|
862
|
+
// that source, so `context.subscription` is the payload most likely to carry it.
|
|
734
863
|
exports.SubscriptionSource = {
|
|
735
864
|
appleAppStore: 0,
|
|
736
865
|
googlePlayStore: 1,
|
|
737
866
|
amazonAppstore: 2,
|
|
738
867
|
huaweiAppGallery: 3,
|
|
739
|
-
|
|
868
|
+
webCheckoutStripe: 4,
|
|
869
|
+
none: 5
|
|
740
870
|
}
|
|
741
871
|
|
|
742
872
|
exports.PlanType = {
|