@tinkerlandai/longplay-react-native 0.10.8 → 0.10.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/index.cjs +2 -2
  2. package/dist/index.d.cts +26549 -12669
  3. package/dist/index.d.ts +26549 -12669
  4. package/dist/index.js +2 -2
  5. package/native-sdk/android/assets/indiana/index.html +40 -40
  6. package/native-sdk/android/java/ai/tinkerland/indiana/BundledWebView.kt +6 -2
  7. package/native-sdk/android/java/ai/tinkerland/indiana/CustomerSession.kt +9 -3
  8. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelConfigurationRelay.kt +47 -21
  9. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelProcess.kt +120 -0
  10. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelSession.kt +57 -16
  11. package/native-sdk/android/java/ai/tinkerland/indiana/HTTPTransport.kt +48 -20
  12. package/native-sdk/android/java/ai/tinkerland/indiana/IndianaClient.kt +3 -1
  13. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallSession.kt +82 -59
  14. package/native-sdk/android/java/ai/tinkerland/indiana/SDKVersion.kt +1 -1
  15. package/native-sdk/android/java/ai/tinkerland/indiana/WireSchema.kt +3 -1
  16. package/native-sdk/android/java/ai/tinkerland/longplay/Longplay.kt +3 -0
  17. package/native-sdk/android/resources/funnel-wire-schemas.json +1 -1
  18. package/native-sdk/ios/BundledWebView.swift +5 -1
  19. package/native-sdk/ios/CustomerSession.swift +7 -3
  20. package/native-sdk/ios/FunnelConfigurationRelay.swift +1 -0
  21. package/native-sdk/ios/FunnelProcess.swift +148 -0
  22. package/native-sdk/ios/FunnelSession.swift +37 -9
  23. package/native-sdk/ios/HTTPTransport.swift +2 -0
  24. package/native-sdk/ios/NativeReceiptRelay.swift +3 -1
  25. package/native-sdk/ios/PaywallSession.swift +5 -4
  26. package/native-sdk/ios/SDKVersion.swift +1 -1
  27. package/native-sdk/ios/WebView/index.html +40 -40
  28. package/native-sdk/ios/funnel-wire-schemas.json +1 -1
  29. package/package.json +1 -1
@@ -1,7 +1,6 @@
1
1
  package ai.tinkerland.indiana
2
2
 
3
3
  import kotlinx.coroutines.sync.Mutex
4
- import kotlinx.coroutines.sync.withLock
5
4
  import org.json.JSONObject
6
5
  import org.json.JSONArray
7
6
  import java.net.URI
@@ -20,6 +19,7 @@ class IndianaPaywallSession internal constructor(
20
19
  private val quoted = AtomicReference<JSONObject?>(null)
21
20
  private val purchased = AtomicReference<JSONObject?>(null)
22
21
  private val mutex = Mutex()
22
+ private val stateLock = Any()
23
23
  private var pendingPreview: String? = null
24
24
  @Volatile private var invalidation: (() -> Unit)? = null
25
25
  private val selector: JSONObject
@@ -29,8 +29,17 @@ class IndianaPaywallSession internal constructor(
29
29
  else JSONObject().put("type", "version").put("versionId", versionId.toString())
30
30
  }
31
31
  val current: JSONObject? get() = view.get()?.let { JSONObject(it.toString()) }
32
- fun close() { token.set(null); view.set(null); quoted.set(null); purchased.set(null); invalidation?.invoke(); invalidation = null }
33
- internal fun onInvalidation(callback: () -> Unit) { invalidation = callback; if (token.get() == null) callback() }
32
+ fun close() {
33
+ val callback = synchronized(stateLock) {
34
+ token.set(null); view.set(null); quoted.set(null); purchased.set(null); pendingPreview = null
35
+ val current = invalidation; invalidation = null; current
36
+ }
37
+ callback?.invoke()
38
+ }
39
+ internal fun onInvalidation(callback: () -> Unit) {
40
+ val closed = synchronized(stateLock) { if (token.get() == null) true else { invalidation = callback; false } }
41
+ if (closed) callback()
42
+ }
34
43
  internal fun hostConfiguration() = configuration
35
44
  internal fun hostInit() = JSONObject().put("event", "init").put("surface", "paywall")
36
45
  .put("scope", JSONObject().put("projectId", configuration.scope.projectId.toString()).put("environment", configuration.scope.environment.value))
@@ -51,64 +60,78 @@ class IndianaPaywallSession internal constructor(
51
60
  return result
52
61
  }
53
62
  suspend fun refresh(): JSONObject = hostCommand(JSONObject().put("operation", "resolve").put("input", JSONObject()))
54
- internal suspend fun hostCommand(payload: JSONObject): JSONObject = mutex.withLock {
55
- if (token.get() == null || payload.keys().asSequence().toSet() != setOf("operation", "input")) throw IndianaClientException("invalid_command")
56
- val input = payload.getJSONObject("input")
57
- when (val action = payload.getString("operation")) {
58
- "resolve" -> {
59
- if (input.length() != 0) throw IndianaClientException("invalid_command")
60
- val prior = view.get()
61
- val selected = prior?.let { JSONObject().put("type", "version").put("versionId", it.getString("id")) } ?: selector
62
- val result = send("resolvePaywall", "paywalls/resolve", JSONObject().put("selector", selected)
63
- .put("commerce", JSONObject().put("provider", provider.value).put("currency", currency)).put("locale", locale)).getJSONObject("paywall")
64
- val key = if (selector.getString("type") == "version") "id" else "paywallId"
65
- if (result.getString(key) != selector.getString(if (key == "id") "versionId" else key) ||
66
- prior != null && (result.getString("id") != prior.getString("id") || result.getJSONObject("offering").getString("id") != prior.getJSONObject("offering").getString("id") || result.getJSONObject("content").getJSONObject("artifact").getString("sha256") != prior.getJSONObject("content").getJSONObject("artifact").getString("sha256")) ||
67
- result.getJSONObject("document").getString("offeringVersionId") != result.getJSONObject("offering").getString("id")) throw IndianaClientException("invalid_response")
68
- view.set(result); JSONObject(result.toString())
69
- }
70
- "preview" -> {
71
- if (!setOf("actionId", "productVersionId", "requestId", "offerId", "nativeOffer", "featureQuantities").containsAll(input.keys().asSequence().toSet())) throw IndianaClientException("invalid_command")
72
- val product = input.getString("productVersionId")
73
- val policy = view.get()?.getJSONObject("document")?.getJSONObject("buttons")?.optJSONObject(input.getString("actionId")) ?: throw IndianaClientException("invalid_command")
74
- if (policy.optString("type") != "checkout") throw IndianaClientException("invalid_command")
75
- val selection = policy.getJSONObject("product")
76
- val allowed = if (selection.getString("type") == "fixed") selection.getString("productVersionId") == product
77
- else selection.getJSONArray("allowedProductVersionIds").let { ids -> (0 until ids.length()).any { ids.getString(it) == product } }
78
- if (!allowed) throw IndianaClientException("invalid_command")
79
- val fingerprint = canonical(payload)
80
- if (pendingPreview != null && pendingPreview != fingerprint) {
81
- val status = purchased.get()?.optString("status")
82
- if (status == null || status == "pending") throw IndianaClientException("command_pending")
83
- quoted.set(null); purchased.set(null)
63
+ internal suspend fun hostCommand(payload: JSONObject): JSONObject {
64
+ if (!mutex.tryLock()) throw IndianaClientException("command_pending")
65
+ try {
66
+ if (token.get() == null || payload.keys().asSequence().toSet() != setOf("operation", "input")) throw IndianaClientException("invalid_command")
67
+ val input = payload.getJSONObject("input")
68
+ return when (val action = payload.getString("operation")) {
69
+ "resolve" -> {
70
+ if (input.length() != 0) throw IndianaClientException("invalid_command")
71
+ val prior = view.get()
72
+ val selected = prior?.let { JSONObject().put("type", "version").put("versionId", it.getString("id")) } ?: selector
73
+ val result = send("resolvePaywall", "paywalls/resolve", JSONObject().put("selector", selected)
74
+ .put("commerce", JSONObject().put("provider", provider.value).put("currency", currency)).put("locale", locale)).getJSONObject("paywall")
75
+ val key = if (selector.getString("type") == "version") "id" else "paywallId"
76
+ if (result.getString(key) != selector.getString(if (key == "id") "versionId" else key) ||
77
+ prior != null && (result.getString("id") != prior.getString("id") || result.getJSONObject("offering").getString("id") != prior.getJSONObject("offering").getString("id") || result.getJSONObject("content").getJSONObject("artifact").getString("sha256") != prior.getJSONObject("content").getJSONObject("artifact").getString("sha256")) ||
78
+ result.getJSONObject("document").getString("offeringVersionId") != result.getJSONObject("offering").getString("id")) throw IndianaClientException("invalid_response")
79
+ synchronized(stateLock) {
80
+ if (token.get() == null) throw IndianaClientException("identity_changed")
81
+ view.set(result); JSONObject(result.toString())
82
+ }
84
83
  }
85
- if (pendingPreview == fingerprint) quoted.get()?.let { return@withLock JSONObject(it.toString()) }
86
- val selectedPlan = JSONObject().put("productVersionId", product)
87
- if (input.has("featureQuantities")) selectedPlan.put("featureQuantities", input.getJSONObject("featureQuantities"))
88
- val body = JSONObject().put("requestId", input.getString("requestId"))
89
- .put("plans", JSONArray().put(selectedPlan)).put("provider", provider.value).put("currency", currency)
90
- for (key in listOf("offerId", "nativeOffer")) if (input.has(key)) body.put(key, input.get(key))
91
- IndianaSchema.validate("previewAttachRequest", body)
92
- pendingPreview = fingerprint
93
- val result = send("previewAttach", "subscriptions/preview-attach", body).getJSONObject("quote")
94
- if (result.getString("provider") != provider.value || result.getString("currency") != currency || result.getJSONArray("lines").length() != 1 || result.getJSONArray("lines").getJSONObject(0).getString("productVersionId") != product) throw IndianaClientException("invalid_response")
95
- quoted.set(result); purchased.set(null); JSONObject(result.toString())
96
- }
97
- "confirm", "cancel", "read" -> {
98
- if (input.length() != 0) throw IndianaClientException("invalid_command")
99
- val id = quoted.get()?.getString("operationId") ?: throw IndianaClientException("invalid_command")
100
- val body = JSONObject().put("operationId", id)
101
- if (action != "read") body.put("intent", action)
102
- val result = send(if (action == "read") "readSubscriptionOperation" else "attach",
103
- if (action == "read") "subscriptions/operations/read" else "subscriptions/attach", body).getJSONObject("operation")
104
- if (result.getString("operationId") != id || result.getString("currency") != currency) throw IndianaClientException("invalid_response")
105
- purchased.get()?.takeIf { it.getString("status") == "complete" }?.let { return@withLock JSONObject(it.toString()) }
106
- purchased.set(result)
107
- if (result.getString("status") !in setOf("pending", "complete")) { pendingPreview = null; quoted.set(null) }
108
- JSONObject(result.toString())
84
+ "preview" -> {
85
+ if (!setOf("actionId", "productVersionId", "requestId", "offerId", "nativeOffer", "featureQuantities").containsAll(input.keys().asSequence().toSet())) throw IndianaClientException("invalid_command")
86
+ val product = input.getString("productVersionId")
87
+ val policy = view.get()?.getJSONObject("document")?.getJSONObject("buttons")?.optJSONObject(input.getString("actionId")) ?: throw IndianaClientException("invalid_command")
88
+ if (policy.optString("type") != "checkout") throw IndianaClientException("invalid_command")
89
+ val selection = policy.getJSONObject("product")
90
+ val allowed = if (selection.getString("type") == "fixed") selection.getString("productVersionId") == product
91
+ else selection.getJSONArray("allowedProductVersionIds").let { ids -> (0 until ids.length()).any { ids.getString(it) == product } }
92
+ if (!allowed) throw IndianaClientException("invalid_command")
93
+ val fingerprint = canonical(payload)
94
+ if (pendingPreview != null && pendingPreview != fingerprint) {
95
+ val status = purchased.get()?.optString("status")
96
+ if (status == null || status == "pending") throw IndianaClientException("command_pending")
97
+ }
98
+ if (pendingPreview == fingerprint) quoted.get()?.let { return JSONObject(it.toString()) }
99
+ val selectedPlan = JSONObject().put("productVersionId", product)
100
+ if (input.has("featureQuantities")) selectedPlan.put("featureQuantities", input.getJSONObject("featureQuantities"))
101
+ val body = JSONObject().put("requestId", input.getString("requestId"))
102
+ .put("plans", JSONArray().put(selectedPlan)).put("provider", provider.value).put("currency", currency)
103
+ for (key in listOf("offerId", "nativeOffer")) if (input.has(key)) body.put(key, input.get(key))
104
+ IndianaSchema.validate("previewAttachRequest", body)
105
+ synchronized(stateLock) {
106
+ if (token.get() == null) throw IndianaClientException("identity_changed")
107
+ if (pendingPreview != fingerprint) { quoted.set(null); purchased.set(null) }
108
+ pendingPreview = fingerprint
109
+ }
110
+ val result = send("previewAttach", "subscriptions/preview-attach", body).getJSONObject("quote")
111
+ if (result.getString("provider") != provider.value || result.getString("currency") != currency || result.getJSONArray("lines").length() != 1 || result.getJSONArray("lines").getJSONObject(0).getString("productVersionId") != product) throw IndianaClientException("invalid_response")
112
+ synchronized(stateLock) {
113
+ if (token.get() == null) throw IndianaClientException("identity_changed")
114
+ quoted.set(result); purchased.set(null); JSONObject(result.toString())
115
+ }
116
+ }
117
+ "confirm", "cancel", "read" -> {
118
+ if (input.length() != 0) throw IndianaClientException("invalid_command")
119
+ val id = quoted.get()?.getString("operationId") ?: throw IndianaClientException("invalid_command")
120
+ val body = JSONObject().put("operationId", id)
121
+ if (action != "read") body.put("intent", action)
122
+ val result = send(if (action == "read") "readSubscriptionOperation" else "attach",
123
+ if (action == "read") "subscriptions/operations/read" else "subscriptions/attach", body).getJSONObject("operation")
124
+ if (result.getString("operationId") != id || result.getString("currency") != currency) throw IndianaClientException("invalid_response")
125
+ synchronized(stateLock) {
126
+ if (token.get() == null) throw IndianaClientException("identity_changed")
127
+ purchased.get()?.takeIf { it.getString("status") == "complete" || it.getString("status") != "pending" && result.getString("status") == "pending" }?.let { return JSONObject(it.toString()) }
128
+ purchased.set(result)
129
+ JSONObject(result.toString())
130
+ }
131
+ }
132
+ else -> throw IndianaClientException("invalid_command")
109
133
  }
110
- else -> throw IndianaClientException("invalid_command")
111
- }
134
+ } finally { mutex.unlock() }
112
135
  }
113
136
  internal fun trustedPurchase(value: JSONObject): JSONObject {
114
137
  val saved = purchased.get() ?: throw IndianaClientException("invalid_command")
@@ -2,5 +2,5 @@
2
2
  package ai.tinkerland.indiana
3
3
 
4
4
  object IndianaSDK {
5
- const val version = "0.10.8"
5
+ const val version = "0.10.11"
6
6
  }
@@ -3,8 +3,10 @@ import org.json.JSONObject
3
3
  import org.json.JSONArray
4
4
  import java.net.URI
5
5
  import java.util.UUID
6
+ import java.util.concurrent.ConcurrentHashMap
6
7
  /** Generated contract interpreter for the native wire boundary; no business policy is evaluated here. */
7
8
  internal object IndianaSchema {
9
+ private val patterns = ConcurrentHashMap<String, Regex>()
8
10
  private val schemas: JSONObject by lazy { IndianaSchema::class.java.getResourceAsStream("/funnel-wire-schemas.json")?.bufferedReader(Charsets.UTF_8)?.use { JSONObject(it.readText()) } ?: throw IndianaClientException("invalid_response") }
9
11
  fun validate(name: String, value: JSONObject) { val root = schemas.getJSONObject(name);if (!valid(value, root, root, 0)) throw IndianaClientException("invalid_response") }
10
12
  fun customerResponse(name: String, value: JSONObject): JSONObject { val root = schemas.getJSONObject(name); return adapt(value, root, root, listOf(name), 0) as JSONObject }
@@ -47,7 +49,7 @@ internal object IndianaSchema {
47
49
  }
48
50
  if (value is String) {
49
51
  if (schema.has("minLength") && value.length < schema.getInt("minLength") || schema.has("maxLength") && value.length > schema.getInt("maxLength")) return false
50
- if (schema.has("pattern") && !Regex(schema.getString("pattern")).containsMatchIn(value)) return false
52
+ if (schema.has("pattern") && !patterns.getOrPut(schema.getString("pattern")) { Regex(schema.getString("pattern")) }.containsMatchIn(value)) return false
51
53
  try { when (schema.optString("format")) { "uuid" -> UUID.fromString(value);"date-time" -> utcMillis(value);"uri" -> if (URI(value).scheme == null) return false } } catch (_: Exception) { return false }
52
54
  }
53
55
  if (value is Number) {
@@ -30,3 +30,6 @@ typealias LongplayStoreException = ai.tinkerland.indiana.IndianaStoreException
30
30
  typealias LongplayStoreSubscriptions = ai.tinkerland.indiana.IndianaStoreSubscriptions
31
31
  typealias LongplaySubscriptionPlan = ai.tinkerland.indiana.IndianaSubscriptionPlan
32
32
  typealias LongplaySubscriptionProvider = ai.tinkerland.indiana.IndianaSubscriptionProvider
33
+
34
+ typealias LongplayFunnelProcess = ai.tinkerland.indiana.IndianaFunnelProcess
35
+ typealias LongplayFunnelProcessBridge = ai.tinkerland.indiana.IndianaFunnelProcessBridge