@tinkerlandai/indiana-react-native 0.9.2 → 0.9.5

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 (83) hide show
  1. package/IndianaReactNative.podspec +19 -0
  2. package/README.md +61 -4
  3. package/THIRD_PARTY_NOTICES.md +74 -0
  4. package/android/build.gradle +24 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/java/ai/tinkerland/indiana/reactnative/IndianaBridgeModule.kt +237 -0
  7. package/android/src/main/java/ai/tinkerland/indiana/reactnative/IndianaBridgePackage.kt +11 -0
  8. package/dist/index.cjs +17 -1
  9. package/dist/index.d.cts +57408 -604
  10. package/dist/index.d.ts +57408 -604
  11. package/dist/index.js +17 -1
  12. package/ios/IndianaBridge.m +11 -0
  13. package/ios/IndianaBridge.swift +275 -0
  14. package/native-sdk/android/assets/indiana/index.html +268 -0
  15. package/native-sdk/android/java/ai/tinkerland/indiana/BillingAuthorizationTerms.kt +55 -0
  16. package/native-sdk/android/java/ai/tinkerland/indiana/BundledWebView.kt +90 -0
  17. package/native-sdk/android/java/ai/tinkerland/indiana/ContentSession.kt +45 -0
  18. package/native-sdk/android/java/ai/tinkerland/indiana/ContentView.kt +36 -0
  19. package/native-sdk/android/java/ai/tinkerland/indiana/CustomerOperations.generated.kt +84 -0
  20. package/native-sdk/android/java/ai/tinkerland/indiana/CustomerResponseSemantics.kt +193 -0
  21. package/native-sdk/android/java/ai/tinkerland/indiana/CustomerSemantics.kt +120 -0
  22. package/native-sdk/android/java/ai/tinkerland/indiana/CustomerSession.kt +106 -0
  23. package/native-sdk/android/java/ai/tinkerland/indiana/FeatureClient.kt +65 -0
  24. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelCommandStorage.kt +163 -0
  25. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelConfigurationRelay.kt +50 -0
  26. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelSession.kt +208 -0
  27. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelView.kt +71 -0
  28. package/native-sdk/android/java/ai/tinkerland/indiana/GeneratedProtocol.kt +16 -0
  29. package/native-sdk/android/java/ai/tinkerland/indiana/GoogleStorefront.kt +165 -0
  30. package/native-sdk/android/java/ai/tinkerland/indiana/HTTPTransport.kt +43 -0
  31. package/native-sdk/android/java/ai/tinkerland/indiana/IndianaClient.kt +526 -0
  32. package/native-sdk/android/java/ai/tinkerland/indiana/NativePendingKeystore.kt +81 -0
  33. package/native-sdk/android/java/ai/tinkerland/indiana/NativeReceiptRelay.kt +28 -0
  34. package/native-sdk/android/java/ai/tinkerland/indiana/NativeStoreOffer.kt +24 -0
  35. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallContent.kt +75 -0
  36. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallSession.kt +144 -0
  37. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallView.kt +74 -0
  38. package/native-sdk/android/java/ai/tinkerland/indiana/Protocol.kt +28 -0
  39. package/native-sdk/android/java/ai/tinkerland/indiana/PushInstallation.kt +170 -0
  40. package/native-sdk/android/java/ai/tinkerland/indiana/RendererNavigation.kt +13 -0
  41. package/native-sdk/android/java/ai/tinkerland/indiana/SDKVersion.kt +6 -0
  42. package/native-sdk/android/java/ai/tinkerland/indiana/StoreSubscriptionManagement.kt +19 -0
  43. package/native-sdk/android/java/ai/tinkerland/indiana/WireSchema.kt +76 -0
  44. package/native-sdk/android/resources/funnel-wire-schemas.json +1 -0
  45. package/native-sdk/android/resources/indiana-third-party-notices.txt +101 -0
  46. package/native-sdk/android/resources/renderer-data-url.txt +1 -0
  47. package/native-sdk/ios/AnalyticsReceipt.swift +10 -0
  48. package/native-sdk/ios/AppleStorefront.swift +218 -0
  49. package/native-sdk/ios/BillingAuthorizationTerms.swift +57 -0
  50. package/native-sdk/ios/BundledWebView.swift +87 -0
  51. package/native-sdk/ios/ContentSession.swift +48 -0
  52. package/native-sdk/ios/ContentView.swift +51 -0
  53. package/native-sdk/ios/CustomerOperations.generated.swift +166 -0
  54. package/native-sdk/ios/CustomerResponseSemantics.swift +232 -0
  55. package/native-sdk/ios/CustomerSemantics.swift +145 -0
  56. package/native-sdk/ios/CustomerSession.swift +129 -0
  57. package/native-sdk/ios/FeatureClient.swift +86 -0
  58. package/native-sdk/ios/FunnelCommandStorage.swift +163 -0
  59. package/native-sdk/ios/FunnelConfigurationRelay.swift +48 -0
  60. package/native-sdk/ios/FunnelSession.swift +232 -0
  61. package/native-sdk/ios/FunnelView.swift +125 -0
  62. package/native-sdk/ios/GeneratedProtocol.swift +16 -0
  63. package/native-sdk/ios/HTTPTransport.swift +74 -0
  64. package/native-sdk/ios/IndianaClient.swift +601 -0
  65. package/native-sdk/ios/NativePendingKeychain.swift +52 -0
  66. package/native-sdk/ios/NativeReceiptRelay.swift +28 -0
  67. package/native-sdk/ios/NativeStoreOffer.swift +34 -0
  68. package/native-sdk/ios/PaywallContent.swift +75 -0
  69. package/native-sdk/ios/PaywallSession.swift +124 -0
  70. package/native-sdk/ios/PaywallView.swift +87 -0
  71. package/native-sdk/ios/Protocol.swift +49 -0
  72. package/native-sdk/ios/PushInstallation.swift +154 -0
  73. package/native-sdk/ios/RendererNavigation.swift +8 -0
  74. package/native-sdk/ios/Resources.swift +15 -0
  75. package/native-sdk/ios/SDKVersion.swift +4 -0
  76. package/native-sdk/ios/StoreSubscriptionManagement.swift +11 -0
  77. package/native-sdk/ios/WebView/THIRD_PARTY_NOTICES.txt +101 -0
  78. package/native-sdk/ios/WebView/index.html +268 -0
  79. package/native-sdk/ios/WebView/renderer-data-url.txt +1 -0
  80. package/native-sdk/ios/WireSchema.swift +107 -0
  81. package/native-sdk/ios/funnel-wire-schemas.json +1 -0
  82. package/package.json +8 -3
  83. package/react-native.config.cjs +12 -0
@@ -0,0 +1,55 @@
1
+ package ai.tinkerland.indiana
2
+
3
+ import org.json.JSONObject
4
+ import java.math.BigDecimal
5
+ import java.util.UUID
6
+
7
+ /** Current immutable Stripe authorization terms contain no payment credential. */
8
+ internal object BillingAuthorizationTerms {
9
+ fun validate(terms: JSONObject) {
10
+ fun invalid(): Nothing = throw IndianaClientException("invalid_response")
11
+ fun text(value: JSONObject, key: String): String = value.opt(key) as? String ?: invalid()
12
+ fun keys(value: JSONObject, expected: Set<String>) { if (value.keys().asSequence().toSet() != expected) invalid() }
13
+ fun quantity(value: Any?, positive: Boolean = false): BigDecimal {
14
+ val raw = value as? String ?: invalid()
15
+ if (!raw.matches(Regex("(0|[1-9][0-9]{0,11})(\\.[0-9]{1,6})?"))) invalid()
16
+ val amount = raw.toBigDecimal(); if (positive && amount <= BigDecimal.ZERO) invalid(); return amount
17
+ }
18
+ fun amount(key: String) { if (!text(terms, key).matches(Regex("[1-9][0-9]{0,14}"))) invalid() }
19
+ val kind = text(terms, "kind"); val currency = text(terms, "currency")
20
+ if (kind !in setOf("auto_topup", "postpaid") || !currency.matches(Regex("[A-Z]{3}"))) invalid()
21
+ listOf("productVersionId", "internal_feature_id").forEach { UUID.fromString(text(terms, it)) }
22
+ val base=setOf("kind","productVersionId","internal_feature_id","currency")
23
+ if(kind=="auto_topup") {
24
+ keys(terms,base+setOf("threshold","quantity","purchaseAmountMinor","purchaseUnits")+if(terms.has("purchaseLimit"))setOf("purchaseLimit")else emptySet())
25
+ if(!text(terms,"threshold").matches(Regex("^-?(0|[1-9][0-9]{0,11})(\\.[0-9]{1,6})?$")))invalid()
26
+ if(terms.has("purchaseLimit")) {
27
+ val limit=terms.optJSONObject("purchaseLimit")?:invalid();keys(limit,setOf("interval","intervalCount","limit"))
28
+ val count=limit.opt("intervalCount")as?Number?:invalid();val cap=limit.opt("limit")as?Number?:invalid()
29
+ if(text(limit,"interval") !in setOf("hour","day","week","month") || count.toDouble() !in 1.0..12.0 || count.toDouble()!=count.toInt().toDouble() || cap.toDouble() !in 1.0..1000000.0 || cap.toDouble()!=cap.toInt().toDouble())invalid()
30
+ }
31
+ if(quantity(terms.opt("quantity"),true)<BigDecimal.ONE)invalid()
32
+ quantity(terms.opt("purchaseUnits"),true);amount("purchaseAmountMinor");return
33
+ }
34
+ amount("maxAmountMinorPerPeriod")
35
+ keys(terms, base + setOf("maxAmountMinorPerPeriod","maxOutstandingMinor", "cashPrice")); amount("maxOutstandingMinor")
36
+ val price = terms.optJSONObject("cashPrice") ?: invalid(); keys(price, setOf("currency", "pricing") + if(price.has("billingCycle")) setOf("billingCycle") else emptySet())
37
+ if (text(price, "currency") != currency) invalid()
38
+ if(price.has("billingCycle")) {
39
+ val cycle=price.optJSONObject("billingCycle") ?: invalid();keys(cycle,setOf("interval","count","anchor"))
40
+ val count=cycle.opt("count") as? Number ?: invalid()
41
+ if(text(cycle,"interval") !in setOf("month","year") || text(cycle,"anchor") !in setOf("membership","calendar") || count.toDouble() !in 1.0..12.0 || count.toDouble() != count.toInt().toDouble()) invalid()
42
+ }
43
+ val pricing = price.optJSONObject("pricing") ?: invalid(); quantity(pricing.opt("units"), true)
44
+ val pricingKind = text(pricing, "kind")
45
+ if (pricingKind == "flat") { keys(pricing, setOf("kind", "units", "unitAmountMinor")); quantity(pricing.opt("unitAmountMinor")); return }
46
+ if (pricingKind !in setOf("graduated", "volume")) invalid(); keys(pricing, setOf("kind", "units", "tiers"))
47
+ val tiers = pricing.optJSONArray("tiers") ?: invalid(); if (tiers.length() !in 1..12) invalid()
48
+ var previous = BigDecimal.ZERO
49
+ for (index in 0 until tiers.length()) {
50
+ val tier = tiers.getJSONObject(index); keys(tier, setOf("upTo", "unitAmountMinor")); quantity(tier.opt("unitAmountMinor"))
51
+ if (index == tiers.length() - 1) { if (!tier.isNull("upTo")) invalid() }
52
+ else { val ceiling = quantity(tier.opt("upTo"), true); if (ceiling <= previous) invalid(); previous = ceiling }
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,90 @@
1
+ package ai.tinkerland.indiana
2
+
3
+ import android.app.Activity
4
+ import android.graphics.Color
5
+ import android.net.Uri
6
+ import android.os.Handler
7
+ import android.os.Looper
8
+ import android.webkit.*
9
+ import kotlinx.coroutines.CoroutineScope
10
+ import kotlinx.coroutines.launch
11
+ import org.json.JSONObject
12
+ import java.net.URI
13
+ import java.util.UUID
14
+
15
+ /** One local renderer/bridge implementation for Funnel and independent Paywall. */
16
+ internal class IndianaBundledWebView(
17
+ activity: Activity, label: String, private val work: CoroutineScope,
18
+ private val dispatch: suspend (String, Any?) -> Any?, private val onError: (Exception) -> Unit
19
+ ) {
20
+ val view = WebView(activity)
21
+ private val shell = URI("https://indiana-sdk.invalid/")
22
+ private var port: WebMessagePort? = null
23
+ private var stopped = false
24
+ private val requests = mutableSetOf<UUID>()
25
+ init {
26
+ view.setBackgroundColor(Color.TRANSPARENT); view.contentDescription = label; view.isSaveEnabled = false
27
+ view.settings.apply {
28
+ javaScriptEnabled = true; domStorageEnabled = false; databaseEnabled = false
29
+ allowFileAccess = false; allowContentAccess = false; mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
30
+ cacheMode = WebSettings.LOAD_NO_CACHE; javaScriptCanOpenWindowsAutomatically = false; setSupportMultipleWindows(false)
31
+ mediaPlaybackRequiresUserGesture = true; saveFormData = false
32
+ }
33
+ view.webViewClient = object: WebViewClient() {
34
+ override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? {
35
+ if (request.url.host != "indiana-sdk.invalid") return null
36
+ return if (request.method == "GET" && request.url.toString() == shell.toString())
37
+ WebResourceResponse("text/html", "UTF-8", activity.assets.open("indiana/index.html"))
38
+ else WebResourceResponse("text/plain", "UTF-8", 404, "Not Found", emptyMap(), java.io.ByteArrayInputStream(byteArrayOf()))
39
+ }
40
+ override fun shouldOverrideUrlLoading(webView: WebView, request: WebResourceRequest): Boolean {
41
+ val url = try { URI(request.url.toString()) } catch (_: Exception) { return true }
42
+ return if (request.isForMainFrame) url != shell else !allowsFunnelRendererNavigation(url, shell)
43
+ }
44
+ override fun onPageFinished(webView: WebView, url: String) {
45
+ if (stopped || url != shell.toString()) return
46
+ port?.close()
47
+ val channels = view.createWebMessageChannel(); port = channels[0]
48
+ channels[0].setWebMessageCallback(object: WebMessagePort.WebMessageCallback() {
49
+ override fun onMessage(port: WebMessagePort, message: WebMessage) { receive(message.data ?: return) }
50
+ }, Handler(Looper.getMainLooper()))
51
+ val nonce = UUID.randomUUID().toString()
52
+ view.evaluateJavascript("window.__indianaFunnelPreparePort(" + JSONObject.quote(nonce) + ")") { accepted ->
53
+ if (!stopped && view.url == shell.toString() && accepted == "true")
54
+ view.postWebMessage(WebMessage(JSONObject().put("type", "indiana-native-port").put("nonce", nonce).toString(), arrayOf(channels[1])), Uri.parse(shell.toString()))
55
+ else { channels[0].close(); channels[1].close() }
56
+ }
57
+ }
58
+ override fun onReceivedError(webView: WebView, request: WebResourceRequest, error: WebResourceError) {
59
+ if (!stopped && request.isForMainFrame) onError(IndianaClientException("renderer_unavailable"))
60
+ }
61
+ }
62
+ }
63
+ fun start() { if (!stopped) view.loadUrl(shell.toString()) }
64
+ private fun receive(raw: String) {
65
+ if (stopped || raw.toByteArray().size > 262144) return
66
+ val message = try { JSONObject(raw) } catch (_: Exception) { return }
67
+ if (message.keys().asSequence().toSet() != setOf("id", "method", "payload")) return
68
+ val id = try { UUID.fromString(message.getString("id")) } catch (_: Exception) { return }
69
+ if (requests.size >= 8 || !requests.add(id)) return
70
+ work.launch {
71
+ try {
72
+ val result = dispatch(message.getString("method"), message.opt("payload"))
73
+ send(JSONObject().put("id", id.toString()).put("result", result ?: JSONObject.NULL))
74
+ } catch (error: Exception) {
75
+ if (!stopped) {
76
+ val detail = JSONObject().put("code", if (error is IndianaClientException) error.code else "NATIVE_OPERATION_FAILED")
77
+ if (error is IndianaClientException) error.code.removePrefix("request_failed_").toIntOrNull()?.let { detail.put("status", it) }
78
+ send(JSONObject().put("id", id.toString()).put("error", detail)); onError(error)
79
+ }
80
+ } finally { requests.remove(id) }
81
+ }
82
+ }
83
+ fun send(value: JSONObject) { if (!stopped) port?.postMessage(WebMessage(value.toString())) }
84
+ fun event(name: String) { send(JSONObject().put("event", name)) }
85
+ fun destroy() {
86
+ if (stopped) return
87
+ event("invalidated"); stopped = true; port?.close(); port = null; requests.clear()
88
+ view.stopLoading(); view.destroy()
89
+ }
90
+ }
@@ -0,0 +1,45 @@
1
+ package ai.tinkerland.indiana
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+ import java.util.UUID
6
+ import kotlinx.coroutines.sync.Mutex
7
+ import kotlinx.coroutines.sync.withLock
8
+
9
+ /** Version-pinned independent Page, Popup or Banner. No Funnel is created. */
10
+ class IndianaContentSession internal constructor(private val configuration: IndianaConfiguration, input: JSONObject, private val locale: String, inputs: JSONObject, private val send: suspend (JSONObject) -> JSONObject) {
11
+ private val mutex = Mutex()
12
+ private val input = JSONObject(input.toString())
13
+ private val inputs = JSONObject(inputs.toString())
14
+ @Volatile private var stopped = false
15
+ @Volatile private var value: JSONObject? = null
16
+ @Volatile private var invalidation: (() -> Unit)? = null
17
+ val current: JSONObject? get() = value?.let { JSONObject(it.toString()) }
18
+ init { require(locale.isNotEmpty() && locale.length <= 35 && inputs.length() <= 40) }
19
+ fun close() { if (stopped) return; stopped = true; value = null; invalidation?.invoke(); invalidation = null }
20
+ suspend fun refresh(): JSONObject = mutex.withLock {
21
+ if (stopped) throw IndianaClientException("identity_changed")
22
+ val content = send(JSONObject(input.toString())).getJSONObject("content")
23
+ if (stopped || content.getString("projectId") != configuration.scope.projectId.toString() || content.getString("kind") != input.getString("kind") || (value != null && value!!.getString("id") != content.getString("id"))) throw IndianaClientException("invalid_response")
24
+ input.put("selector", JSONObject().put("type", "version").put("id", content.getString("id")))
25
+ value = JSONObject(content.toString()); JSONObject(content.toString())
26
+ }
27
+ internal fun onInvalidation(callback: () -> Unit) { invalidation = callback; if (stopped) callback() }
28
+ internal fun hostInit(): JSONObject = JSONObject().put("event", "init").put("surface", "content").put("scope", JSONObject().put("projectId", configuration.scope.projectId.toString()).put("environment", configuration.scope.environment.value)).put("apiOrigin", configuration.apiOrigin)
29
+ internal suspend fun hostContent(): JSONObject {
30
+ if (stopped) throw IndianaClientException("identity_changed")
31
+ return JSONObject().put("content", current ?: refresh()).put("locale", locale).put("inputs", JSONObject(inputs.toString()))
32
+ }
33
+ internal fun trustedAction(raw: JSONObject): JSONObject = safe {
34
+ if (stopped || raw.keys().asSequence().toSet() != setOf("requestId", "contentVersionId", "actionId", "event", "value", "answers", "selection", "requestedClose")) throw IndianaClientException("invalid_command")
35
+ UUID.fromString(raw.getString("requestId"))
36
+ val content = value ?: throw IndianaClientException("invalid_command")
37
+ val document = content.getJSONObject("document")
38
+ val id = raw.getString("actionId")
39
+ val actions = document.getJSONObject("screen").getJSONArray("actions")
40
+ val action = (0 until actions.length()).map { actions.getJSONObject(it) }.firstOrNull { it.getString("id") == id } ?: throw IndianaClientException("invalid_command")
41
+ val properties = action.getJSONObject("properties")
42
+ if (raw.getString("contentVersionId") != content.getString("id") || !document.getJSONObject("contract").getJSONObject("outputs").has(id) || action.getString("type") != "event" || raw.getString("event") != action.getString("event") || raw.getBoolean("requestedClose") != action.getBoolean("close") || !IndianaSchema.equal(raw.opt("value"), if (properties.has("value")) properties.opt("value") else properties) || raw.optJSONObject("answers") == null || raw.optJSONObject("selection") == null) throw IndianaClientException("invalid_command")
43
+ JSONObject(raw.toString())
44
+ }
45
+ }
@@ -0,0 +1,36 @@
1
+ package ai.tinkerland.indiana
2
+
3
+ import android.app.Activity
4
+ import android.widget.FrameLayout
5
+ import kotlinx.coroutines.CoroutineScope
6
+ import kotlinx.coroutines.Dispatchers
7
+ import kotlinx.coroutines.SupervisorJob
8
+ import kotlinx.coroutines.cancel
9
+ import org.json.JSONObject
10
+
11
+ /** Independent content; actions are explicit callbacks into the merchant host. */
12
+ class IndianaContentView(activity: Activity, val session: IndianaContentSession): FrameLayout(activity) {
13
+ var onAction: (suspend (JSONObject) -> Unit)? = null
14
+ var onDismiss: (() -> Unit)? = null
15
+ var onError: ((Exception) -> Unit)? = null
16
+ private val work = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
17
+ private val host = IndianaBundledWebView(activity, "Indiana content", work, ::dispatch) { error -> onError?.invoke(error) }
18
+ private var stopped = false
19
+ init {
20
+ addView(host.view, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
21
+ session.onInvalidation { post { destroy() } }; host.start()
22
+ }
23
+ private suspend fun dispatch(method: String, raw: Any?): Any? = when (method) {
24
+ "ready" -> { host.send(session.hostInit()); null }
25
+ "content" -> session.hostContent()
26
+ "contentAction" -> {
27
+ val trusted = session.trustedAction(raw as? JSONObject ?: throw IndianaClientException("invalid_command"))
28
+ (onAction ?: throw IndianaClientException("unsupported_capability"))(trusted); null
29
+ }
30
+ "dismiss" -> { close(); null }
31
+ "error" -> { onError?.invoke(IndianaClientException("renderer_failed")); null }
32
+ else -> throw IndianaClientException("invalid_command")
33
+ }
34
+ fun destroy() { if (stopped) return; stopped = true; host.destroy(); work.cancel(); removeView(host.view); session.close(); onDismiss?.invoke() }
35
+ fun close() = destroy()
36
+ }
@@ -0,0 +1,84 @@
1
+ // Generated by scripts/sdk/generate-native-protocol.mjs. Do not edit.
2
+ package ai.tinkerland.indiana
3
+ import org.json.JSONObject
4
+
5
+ enum class IndianaCustomerOperation(val wireName: String, internal val path: String, internal val method: String) {
6
+ bindPushInstallation("bindPushInstallation", "push/installations/bind", "POST"),
7
+ readPushInstallation("readPushInstallation", "push/installations/read", "POST"),
8
+ updatePushAddress("updatePushAddress", "push/installations/address", "POST"),
9
+ readMessagingPreference("readMessagingPreference", "messaging/preferences/read", "POST"),
10
+ setMessagingPreference("setMessagingPreference", "messaging/preferences/set", "POST"),
11
+ getFeatures("getFeatures", "features", "GET"),
12
+ evaluateFeatures("evaluateFeatures", "features/evaluate", "POST"),
13
+ trackEvents("trackEvents", "events", "POST"),
14
+ restoreNativePurchase("restoreNativePurchase", "subscriptions/native/restore", "POST"),
15
+ setupPayment("setupPayment", "billing/setup-payment", "POST"),
16
+ readPaymentSetup("readPaymentSetup", "billing/payment-setup", "POST"),
17
+ previewAttach("previewAttach", "subscriptions/preview-attach", "POST"),
18
+ attach("attach", "subscriptions/attach", "POST"),
19
+ readSubscriptionOperation("readSubscriptionOperation", "subscriptions/operations/read", "POST"),
20
+ previewUpdate("previewUpdate", "subscriptions/preview-update", "POST"),
21
+ update("update", "subscriptions/update", "POST"),
22
+ readSubscriptionUpdate("readSubscriptionUpdate", "subscriptions/updates/read", "POST"),
23
+ readBillingControls("readBillingControls", "billing/controls/read", "POST"),
24
+ prepareBillingAuthorization("prepareBillingAuthorization", "billing/authorizations/prepare", "POST"),
25
+ confirmBillingAuthorization("confirmBillingAuthorization", "billing/authorizations/confirm", "POST"),
26
+ setBillingCoupons("setBillingCoupons", "billing/coupons", "POST"),
27
+ revokeBillingAuthorization("revokeBillingAuthorization", "billing/authorizations/revoke", "POST"),
28
+ listBillingInvoices("listBillingInvoices", "billing/invoices/read", "POST"),
29
+ confirmBillingAdjustment("confirmBillingAdjustment", "billing/invoices/confirm-adjustment", "POST"),
30
+ checkEntitlement("checkEntitlement", "entitlements/check", "POST"),
31
+ resolveContent("resolveContent", "content/resolve", "POST"),
32
+ resolvePaywall("resolvePaywall", "paywalls/resolve", "POST"),
33
+ resolveOffering("resolveOffering", "offerings/resolve", "POST"),
34
+ readHoldings("readHoldings", "customer-holdings", "POST"),
35
+ readLedger("readLedger", "customer-ledger", "POST"),
36
+ readBillingInvoice("readBillingInvoice", "billing/invoices/detail", "POST"),
37
+ readCreditStatement("readCreditStatement", "credit-statements/read", "POST"),
38
+ quoteBalance("quoteBalance", "balances/quote", "POST"),
39
+ redeemReward("redeemReward", "rewards/redeem", "POST"),
40
+ createReferralCode("createReferralCode", "rewards/referral-code", "POST"),
41
+ claimRewardProduct("claimRewardProduct", "rewards/product/claim", "POST"),
42
+ redeemCoupon("redeemCoupon", "offers/redeem", "POST"),
43
+ listOffers("listOffers", "offers", "GET"),
44
+ grantOffer("grantOffer", "offers/grant", "POST");
45
+ }
46
+ suspend fun IndianaClient.bindPushInstallation(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.bindPushInstallation, input)
47
+ suspend fun IndianaClient.readPushInstallation(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readPushInstallation, input)
48
+ suspend fun IndianaClient.updatePushAddress(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.updatePushAddress, input)
49
+ suspend fun IndianaClient.readMessagingPreference(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readMessagingPreference, input)
50
+ suspend fun IndianaClient.setMessagingPreference(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.setMessagingPreference, input)
51
+ suspend fun IndianaClient.getFeatures(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.getFeatures, input)
52
+ suspend fun IndianaClient.evaluateFeatures(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.evaluateFeatures, input)
53
+ suspend fun IndianaClient.trackEvents(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.trackEvents, input)
54
+ suspend fun IndianaClient.restoreNativePurchase(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.restoreNativePurchase, input)
55
+ suspend fun IndianaClient.setupPayment(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.setupPayment, input)
56
+ suspend fun IndianaClient.readPaymentSetup(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readPaymentSetup, input)
57
+ suspend fun IndianaClient.previewAttach(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.previewAttach, input)
58
+ suspend fun IndianaClient.attach(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.attach, input)
59
+ suspend fun IndianaClient.readSubscriptionOperation(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readSubscriptionOperation, input)
60
+ suspend fun IndianaClient.previewUpdate(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.previewUpdate, input)
61
+ suspend fun IndianaClient.update(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.update, input)
62
+ suspend fun IndianaClient.readSubscriptionUpdate(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readSubscriptionUpdate, input)
63
+ suspend fun IndianaClient.readBillingControls(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readBillingControls, input)
64
+ suspend fun IndianaClient.prepareBillingAuthorization(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.prepareBillingAuthorization, input)
65
+ suspend fun IndianaClient.confirmBillingAuthorization(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.confirmBillingAuthorization, input)
66
+ suspend fun IndianaClient.setBillingCoupons(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.setBillingCoupons, input)
67
+ suspend fun IndianaClient.revokeBillingAuthorization(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.revokeBillingAuthorization, input)
68
+ suspend fun IndianaClient.listBillingInvoices(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.listBillingInvoices, input)
69
+ suspend fun IndianaClient.confirmBillingAdjustment(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.confirmBillingAdjustment, input)
70
+ suspend fun IndianaClient.checkEntitlement(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.checkEntitlement, input)
71
+ suspend fun IndianaClient.resolveContent(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.resolveContent, input)
72
+ suspend fun IndianaClient.resolvePaywall(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.resolvePaywall, input)
73
+ suspend fun IndianaClient.resolveOffering(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.resolveOffering, input)
74
+ suspend fun IndianaClient.readHoldings(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readHoldings, input)
75
+ suspend fun IndianaClient.readLedgerRequest(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readLedger, input)
76
+ suspend fun IndianaClient.readBillingInvoice(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readBillingInvoice, input)
77
+ suspend fun IndianaClient.readCreditStatement(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.readCreditStatement, input)
78
+ suspend fun IndianaClient.quoteBalance(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.quoteBalance, input)
79
+ suspend fun IndianaClient.redeemReward(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.redeemReward, input)
80
+ suspend fun IndianaClient.createReferralCode(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.createReferralCode, input)
81
+ suspend fun IndianaClient.claimRewardProduct(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.claimRewardProduct, input)
82
+ suspend fun IndianaClient.redeemCoupon(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.redeemCoupon, input)
83
+ suspend fun IndianaClient.listOffers(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.listOffers, input)
84
+ suspend fun IndianaClient.grantOffer(input: JSONObject): JSONObject = customerOperation(IndianaCustomerOperation.grantOffer, input)
@@ -0,0 +1,193 @@
1
+ package ai.tinkerland.indiana
2
+
3
+ import org.json.JSONObject
4
+ import org.json.JSONArray
5
+ import java.math.BigDecimal
6
+ import java.net.URI
7
+
8
+ /** Dispatch is based on generated schema fields, never arbitrary merchant JSON shapes. */
9
+ internal object IndianaCustomerResponseSemantics {
10
+ private fun require(value: Boolean) { if (!value) throw IndianaClientException("invalid_response") }
11
+ private fun o(value: Any?) = value as? JSONObject ?: JSONObject()
12
+ private fun a(value: Any?) = (value as? JSONArray)?.let { array -> (0 until array.length()).map { o(array.opt(it)) } } ?: emptyList()
13
+ private fun s(value: Any?) = value as? String ?: ""
14
+ private fun n(value: Any?) = when (value) { is String -> value.toBigDecimalOrNull() ?: BigDecimal.ZERO; is Number -> value.toString().toBigDecimal(); else -> BigDecimal.ZERO }
15
+ private fun nil(value: Any?) = value == null || value == JSONObject.NULL
16
+ private fun unique(values: List<String>) = values.distinct().size == values.size
17
+ private fun time(value: Any?) = utcMillis(s(value))
18
+ private fun strings(value: Any?) = (value as? JSONArray)?.let { array -> (0 until array.length()).map { array.getString(it) } } ?: emptyList()
19
+ fun rules(original: Any?, schema: JSONObject): Any? {
20
+ val rules = schema.optJSONArray("x-indiana-rules")?.let { strings(it).toSet() } ?: emptySet()
21
+ val value = if ("trim" in rules && original is String) original.trim { it in "\u0009\u000a\u000b\u000c\u000d\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff" } else original
22
+ if ("positive" in rules) require(n(value) > BigDecimal.ZERO); if ("atLeastOne" in rules) require(n(value) >= BigDecimal.ONE)
23
+ if ("lowercase" in rules) require(s(value) == s(value).lowercase())
24
+ if ("https" in rules) { val url = URI(s(value)); require(url.scheme == "https" && !url.host.isNullOrEmpty()) }
25
+ if ("locale" in rules) { val parts = s(value).split('-'); require(parts.first() !in IndianaProtocol.nonCanonicalLocaleLanguages && parts.drop(1).none { if (it.length == 4) it in IndianaProtocol.nonCanonicalLocaleScripts else it in IndianaProtocol.nonCanonicalLocaleRegions }) }
26
+ if ("notNegativeZero" in rules) require(!s(value).startsWith('-') || n(value).compareTo(BigDecimal.ZERO) != 0)
27
+ if ("boundedAdjustment" in rules) require(s(value) != "-0" && n(value) >= BigDecimal(-200000000) && n(value) <= BigDecimal(200000000))
28
+ if ("orderedPair" in rules && value is JSONArray && value.length() == 2) require(n(value.opt(0)) <= n(value.opt(1)))
29
+ if (rules.any { it in listOf("jsonBudget", "attributes", "condition") }) { jsonBudget(value); if (rules.any { it in listOf("attributes", "condition") }) require(o(value).length() <= 128) }
30
+ if ("condition" in rules) condition(o(value))
31
+ return value
32
+ }
33
+ private fun jsonBudget(value: Any?) {
34
+ var count = 0
35
+ fun visit(value: Any?, depth: Int) {
36
+ require(++count <= 10000 && depth <= 20)
37
+ fun text(text: String) { require('\u0000' !in text && text.indices.none { Character.isHighSurrogate(text[it]) && (it + 1 >= text.length || !Character.isLowSurrogate(text[it + 1])) || Character.isLowSurrogate(text[it]) && (it == 0 || !Character.isHighSurrogate(text[it - 1])) }) }
38
+ when (value) { is String -> text(value); is JSONArray -> for (index in 0 until value.length()) visit(value.opt(index), depth + 1); is JSONObject -> for (key in value.keys()) { text(key); require(key !in listOf("__proto__", "constructor", "prototype")); visit(value.opt(key), depth + 1) } }
39
+ }
40
+ visit(value, 0)
41
+ val encoded = if (value is JSONObject || value is JSONArray) value.toString() else JSONArray().put(value).toString().drop(1).dropLast(1)
42
+ require(encoded.toByteArray(Charsets.UTF_8).size <= 65536)
43
+ }
44
+ private fun condition(value: JSONObject) {
45
+ val operators = setOf("\$in", "\$ini", "\$inGroup", "\$nin", "\$nini", "\$notInGroup", "\$gt", "\$gte", "\$lt", "\$lte", "\$regex", "\$regexi", "\$ne", "\$eq", "\$size", "\$elemMatch", "\$all", "\$alli", "\$not", "\$type", "\$exists", "\$vgt", "\$vgte", "\$vlt", "\$vlte", "\$vne", "\$veq", "\$and", "\$or", "\$nor", "\$savedGroup")
46
+ require(value.optString("source") !in listOf("all", "any", "group", "parameter", "answer", "attribute", "result") || !value.has("operator") && !value.has("predicates") && !value.has("groupId"))
47
+ fun visit(current: Any?) {
48
+ if (current is JSONArray) for (index in 0 until current.length()) visit(current.opt(index))
49
+ if (current is JSONObject) for (key in current.keys()) {
50
+ val child = current.opt(key)
51
+ if (key == "\$savedGroup") { val group = o(child); require(group.keys().asSequence().all { it in listOf("id", "attributeKey") } && group.optString("id").matches(IndianaProtocol.uuidPattern) && (!group.has("attributeKey") || group.getString("attributeKey").length in 1..128)) }
52
+ if (key.startsWith("$")) require(key in operators) else require(key.split('.').none { it in listOf("__proto__", "constructor", "prototype") })
53
+ if (key in listOf("\$regex", "\$regexi") && child is String) {
54
+ require(!Regex("\\(\\?[<!=]|\\\\[0-9]|\\\\k<").containsMatchIn(child))
55
+ for (match in Regex("\\{([0-9]+)(?:,([0-9]*))?\\}").findAll(child)) for (index in listOf(1, 2)) require((match.groups[index]?.value?.toIntOrNull() ?: 0) <= 255)
56
+ if (key == "\$regexi") require(child.all { it.code <= 127 })
57
+ val syntax = buildString { var index = 0; while (index < child.length) { if (child[index] == '\\' && index + 1 < child.length) { val next = child[index + 1]; if (next.isLetter() && next.code < 128 && next !in "bBcdDsSwWfnrtvux") append(next) else { append('\\'); append(next) }; index += 2 } else { append(child[index]); index++ } } }
58
+ Regex(syntax)
59
+ }; visit(child)
60
+ }
61
+ }; visit(value)
62
+ }
63
+ fun objectValue(v: JSONObject, fields: Set<String>, path: List<String>) {
64
+ fun has(vararg keys: String) = fields.containsAll(keys.toList())
65
+ if (has("productKind", "seatBundle", "applePromotionalOffer", "replacement", "offerKind")) {
66
+ val apple = v.getString("provider") == "apple"; val member = v.getString("productKind") == "membership"; val kind = v.getString("offerKind"); val replacement = o(v.opt("replacement")); val proof = o(v.opt("applePromotionalOffer"))
67
+ if (replacement.length() > 0) require(member && kind == "none" && v.isNull("trialDays") && replacement.getString("policy") == if (apple) "apple_store" else "google_full_price")
68
+ if (!member) require(v.isNull("trialDays") && kind == "none" && !v.has("basePlanId")) else if (!apple) require(v.has("basePlanId"))
69
+ require(n(v.opt("discountMinor")) <= n(v.opt("catalogAmountMinor")) && unique(a(v.opt("seatBundle")).map { it.getString("key") }))
70
+ require(v.isNull("trialDays") || n(v.opt("discountMinor")).compareTo(BigDecimal.ZERO) == 0 && kind != "none")
71
+ require(kind != "none" || n(v.opt("discountMinor")).compareTo(BigDecimal.ZERO) == 0)
72
+ if (apple) {
73
+ require(!v.isNull("appAccountToken") && !v.has("obfuscatedAccountId") && !v.has("obfuscatedProfileId") && !v.has("basePlanId"))
74
+ require((kind == "promotional") == !v.isNull("applePromotionalOffer") && (kind == "promotional") == v.has("offerId"))
75
+ require(proof.length() == 0 || proof.getString("offerId") == v.optString("offerId"))
76
+ } else require(!v.has("appAccountToken") && v.optString("obfuscatedAccountId").isNotEmpty() && v.optString("obfuscatedProfileId").isNotEmpty() && v.isNull("applePromotionalOffer") && (kind == "none") == !v.has("offerId"))
77
+ }
78
+ if (has("kind", "tiers") && v.optString("kind") in listOf("graduated", "volume")) {
79
+ val rows = a(v.opt("tiers")); require(rows.isNotEmpty() && rows.last().isNull("upTo"))
80
+ for ((index, row) in rows.dropLast(1).withIndex()) require(!row.isNull("upTo") && (index == 0 || n(row.opt("upTo")) > n(rows[index - 1].opt("upTo"))))
81
+ require(rows.none { n(it.opt("flatAmountMinor")) > BigDecimal.ZERO } || v.getString("kind") == "volume" && rows.size > 1)
82
+ }
83
+ if (has("units", "prices", "minimumQuantity", "maximumQuantity")) {
84
+ require(n(v.opt("units")) > BigDecimal.ZERO && n(v.opt("maximumQuantity")) >= n(v.opt("minimumQuantity")))
85
+ val prices = a(v.opt("prices")); require(unique(prices.map { it.getString("currency") }))
86
+ for (price in prices) { require(!price.has("pricing") || n(price.opt("amountMinor")).compareTo(BigDecimal.ZERO) == 0); val bounds = listOf(n(v.opt("maximumQuantity"))) + a(o(price.opt("pricing")).opt("tiers")).filter { !it.isNull("upTo") }.flatMap { listOf(n(it.opt("upTo")), n(it.opt("upTo")) + BigDecimal.ONE) }; for (quantity in bounds) if (quantity <= n(v.opt("maximumQuantity"))) prepaid(price, quantity) }
87
+ }
88
+ if (has("terms", "includedAmount", "priceId", "productId", "unitAmountMinor")) {
89
+ val terms = v.getJSONObject("terms"); val quantity = n(v.opt("quantity"))
90
+ require(quantity >= n(terms.opt("minimumQuantity")) && quantity <= n(terms.opt("maximumQuantity")) && v.isNull("productId") == v.isNull("priceId"))
91
+ require(quantity.compareTo(BigDecimal.ZERO) == 0 || !v.has("pricing") && n(v.opt("unitAmountMinor")).compareTo(BigDecimal.ZERO) == 0 || !v.isNull("priceId"))
92
+ require(n(v.opt("includedAmount")) + n(terms.opt("units")) * quantity <= BigDecimal("999999999999.999999")); prepaid(v, quantity)
93
+ }
94
+ if (has("lineId", "baseAmountMinor", "catalogAmountMinor", "seats", "trial")) {
95
+ val seats = a(v.opt("seats")); require(unique(seats.map { it.getString("key") }))
96
+ require(seats.all { n(it.opt("quantity")) >= n(it.opt("includedQuantity")) && n(it.opt("quantity")) <= n(it.opt("maximumQuantity")) })
97
+ var total = n(v.opt("baseAmountMinor")) + seats.fold(BigDecimal.ZERO) { total, seat -> total + n(seat.opt("unitAmountMinor")) * (n(seat.opt("quantity")) - n(seat.opt("includedQuantity"))) }
98
+ for (row in a(v.opt("prepaid"))) total += prepaid(row, n(row.opt("quantity"))); require(total.compareTo(n(v.opt("catalogAmountMinor"))) == 0)
99
+ }
100
+ if (has("percent_off", "amount_off", "currency", "duration_in_months", "duration")) require(v.has("percent_off") != v.has("amount_off") && !v.has("amount_off") == !v.has("currency") && (v.getString("duration") == "repeating") == v.has("duration_in_months"))
101
+ if (has("operationId", "lines", "discount", "charge", "nativeOffer", "execution")) attachQuote(v)
102
+ if (has("executor", "checkoutUrl", "execution")) {
103
+ require(v.isNull("execution") || v.getString("executor") == "merchant")
104
+ if (has("quoteFinalized", "orderId")) require(v.isNull("checkoutUrl") || v.getString("executor") == "indiana")
105
+ if (!v.isNull("checkoutUrl")) require(URI(v.getString("checkoutUrl")).scheme == "https")
106
+ }
107
+ if (has("action", "executor", "update", "cancel", "schedule", "release")) {
108
+ for (action in listOf("update", "cancel", "schedule", "release")) require(!v.isNull(action) == (v.getString("action") == action))
109
+ require(v.getString("executor") == "indiana" || (a(o(v.opt("update")).opt("items")) + a(o(v.opt("schedule")).opt("nextItems"))).all { !it.isNull("price") })
110
+ }
111
+ if (has("offerKind", "offerId", "discountMinor", "trialDays") && fields.size == 4) require((v.getString("offerKind") != "none" || v.isNull("offerId") && n(v.opt("discountMinor")).compareTo(BigDecimal.ZERO) == 0 && v.isNull("trialDays")) && (v.isNull("trialDays") || n(v.opt("discountMinor")).compareTo(BigDecimal.ZERO) == 0))
112
+ if (has("lookupKey", "displayName", "items", "metadata")) offering(v)
113
+ if (has("tiers", "assignments")) { val tiers = a(v.opt("tiers")).map { it.getString("id") }; val assignments = a(v.opt("assignments")); require(unique(tiers) && unique(assignments.map { it.getString("productVersionId") }) && unique(assignments.map { it.getString("tierId") + ":" + it.getString("period") }) && assignments.all { it.getString("tierId") in tiers }) }
114
+ if (has("kind", "screen", "contract", "name")) { val screen = v.getJSONObject("screen"); val kind = v.getString("kind"); val actions = a(screen.opt("actions")); val outputs = v.getJSONObject("contract").getJSONObject("outputs"); require(screen.getString("surface") == kind && screen.getString("name") == v.getString("name") && (kind == "banner") == (o(screen.opt("presentation")).optString("type") == "inline") && actions.all { it.getString("type") in listOf("event", "navigate") }); require(actions.filter { it.getString("type") == "event" }.map { it.getString("id") }.toSet() == outputs.keys().asSequence().toSet()); require(v.toString().toByteArray(Charsets.UTF_8).size <= 300000) }
115
+ if (has("pages", "actions", "initialPageId", "strings", "surface")) screen(v)
116
+ if (has("type", "planIds", "label", "fallback") && v.optString("type") == "plan_picker") { val ids = strings(v.opt("planIds")); require(unique(ids)); if (v.has("grouping")) require(productGrouping(v.getJSONObject("grouping")) == ids.toSet()) }
117
+ if (has("type", "source", "expired", "granularity") && v.optString("type") == "countdown") require(v.getString("source") != "fixed" || v.has("at"))
118
+ if (has("minimumQuantity", "maximumQuantity", "defaultQuantity", "step")) { val quantity = v.getLong("defaultQuantity"); val minimum = v.getLong("minimumQuantity"); require(quantity >= minimum && quantity <= v.getLong("maximumQuantity") && (quantity - minimum) % v.getLong("step") == 0L) }
119
+ if (has("id", "event", "properties", "type", "close")) require(o(v.opt("properties")).length() <= 20)
120
+ if (has("transactionIds", "membership")) require(unique(strings(v.opt("transactionIds"))))
121
+ if (has("defaultLocale", "locales") && path.last() == "displayCopy") { val locales = v.getJSONObject("locales"); require(locales.length() in 1..20 && locales.has(v.getString("defaultLocale")) && v.toString().toByteArray(Charsets.UTF_8).size <= 50000) }
122
+ if (has("kind", "threshold", "quantity", "purchaseUnits")) require(n(v.opt("quantity")) >= BigDecimal.ONE && n(v.opt("purchaseUnits")) > BigDecimal.ZERO)
123
+ if (has("kind", "cashPrice", "currency")) require(o(v.opt("cashPrice")).optString("currency") == v.getString("currency"))
124
+ if (has("currency", "pricing", "billingCycle")) o(v.opt("pricing")).let { require(it.optString("kind") != "volume" || a(it.opt("tiers")).size == 1) }
125
+ if (has("status", "terms", "acceptedAt", "effectiveAt", "revokedAt")) { val status = v.getString("status"); require((status != "prepared" || v.isNull("acceptedAt")) && (status != "active" || !v.isNull("acceptedAt") && !v.isNull("effectiveAt")) && (status == "revoked") == !v.isNull("revokedAt")) }
126
+ if (has("basis", "thresholds", "limit")) for (field in listOf("thresholds", "includedThresholds", "absoluteThresholds")) v.optJSONArray(field)?.let { for (index in 1 until it.length()) require(n(it.opt(index)) > n(it.opt(index - 1))) }
127
+ if (has("basis", "used", "reserved") && v.optString("basis") == "usage") for (key in listOf("used", "reserved")) require(v.getString(key).matches(Regex("^(0|[1-9][0-9]{0,11})(\\.[0-9]{1,6})?$")))
128
+ if (has("amountMinor", "requestId", "reason") && v.opt("amountMinor") is String) require(v.getString("amountMinor") != "-0" && n(v.opt("amountMinor")) >= BigDecimal(-200000000) && n(v.opt("amountMinor")) <= BigDecimal(200000000))
129
+ if (has("inputs", "outputs") && path.last() == "contract") require(o(v.opt("inputs")).length() <= 40 && o(v.opt("outputs")).length() <= 40)
130
+ if (has("formatVersion", "kind", "sha256", "url") && "paywall" in path) require(v.getString("kind") == "paywall" && v.getInt("formatVersion") == 1)
131
+ if (has("changeKind", "seat", "feature", "effectiveAt")) require((v.getString("changeKind") == "seats") == !v.isNull("seat") && (v.getString("changeKind") == "quantity") == v.has("feature"))
132
+ if (has("usage", "capacity", "remaining", "withinLimit", "operationId")) {
133
+ require(v.isNull("capacity") == v.isNull("remaining")); require(if (v.isNull("capacity")) v.getBoolean("withinLimit") else n(v.opt("remaining")).compareTo(n(v.opt("capacity")) - n(v.opt("usage"))) == 0 && v.getBoolean("withinLimit") == (n(v.opt("remaining")) >= BigDecimal.ZERO))
134
+ require((v.getLong("revision") == 0L) == v.isNull("operationId") && v.isNull("operationId") == v.isNull("updatedAt"))
135
+ }
136
+ if (has("usageType", "accessible", "available", "held", "debt", "usage")) {
137
+ require((v.getString("kind") == "metered") == !v.isNull("usageType") && (v.getBoolean("accessible") || v.getString("available") == "0" && v.getString("held") == "0"))
138
+ val usage = o(v.opt("usage")); if (v.optString("usageType") == "non_consumable") require(usage.length() > 0 && usage.getString("internal_feature_id") == v.getString("internal_feature_id") && v.getBoolean("unlimited") == usage.isNull("capacity") && v.getString("available") == (if (usage.isNull("remaining") || usage.getString("remaining").startsWith('-')) "0" else usage.getString("remaining")) && v.getString("held") == "0" && v.getString("debt") == "0") else require(v.isNull("usage"))
139
+ }
140
+ if (has("customerId", "rows", "nextCursor") && "ledger" in path) { val rows = a(v.opt("rows")); require(unique(rows.map { it.getString("type") + it.getString("id") })); if (!v.isNull("nextCursor")) { val last = rows.lastOrNull() ?: JSONObject(); val cursor = v.getJSONObject("nextCursor"); require(last.optString("id") == cursor.getString("id") && last.optString("type") == cursor.getString("type") && last.optString("occurredAt") == cursor.getString("at")) } }
141
+ if (has("kind", "accepted", "charges", "reservationStatus", "quantity") && v.optString("type") == "usage") {
142
+ val zero = v.getString("kind") == "set_usage" || v.getString("kind") == "consume" && v.getString("quantity") == "0"; val accepted = v.getBoolean("accepted"); val charges = a(v.opt("charges"))
143
+ require(if (zero) accepted && v.isNull("reason") && charges.isEmpty() && v.isNull("reservationStatus") else if (accepted) v.isNull("reason") && charges.isNotEmpty() else !v.isNull("reason") && charges.isEmpty())
144
+ require(!v.isNull("originalId") == (v.getString("kind") in listOf("confirm", "refund", "release", "expire")) && v.isNull("expiresAt") == v.isNull("reservationStatus"))
145
+ }
146
+ if (has("purpose", "productVersionId", "productName", "invoiceId", "originalId") && v.optString("type") == "transaction") { val product = v.getString("purpose") == "product"; require(v.isNull("currency") == v.isNull("amountMinor") && product == !v.isNull("productVersionId") && product == !v.isNull("productName") && product == !v.isNull("productVersion") && product == v.isNull("invoiceId") && (v.getString("kind") == "refund") == !v.isNull("originalId") && (v.getString("kind") != "refund" || v.isNull("amountMinor") || n(v.opt("amountMinor")) > BigDecimal.ZERO)) }
147
+ if (has("net", "internal_feature_id") && v.optString("net").startsWith('-')) require(n(v.opt("net")).compareTo(BigDecimal.ZERO) != 0)
148
+ if (has("funding", "units")) { require(n(v.opt("units")) > BigDecimal.ZERO); if (v.getString("funding") == "prepaid") { val rows = a(v.opt("pricing")); require(rows.fold(BigDecimal.ZERO) { sum, row -> sum + n(row.opt("units")) }.compareTo(n(v.opt("units"))) == 0 && rows.fold(BigDecimal.ZERO) { sum, row -> sum + n(row.opt("amount")) }.compareTo(n(v.opt("amount"))) == 0 && rows.all { (n(it.opt("unitCost")) * n(it.opt("units"))).compareTo(n(it.opt("amount"))) == 0 }) } }
149
+ if (has("allowed", "allocations", "quantity", "internal_feature_id") && v.getBoolean("allowed")) { val rows = a(v.opt("allocations")); val delta = rows.fold(BigDecimal.ZERO) { sum, row -> sum + n(row.opt("units")) } - n(v.opt("quantity")); val tolerance = if (rows.any { it.getString("funding") == "balance" }) BigDecimal("0.0000000001") else BigDecimal.ZERO; require(delta >= -tolerance && delta <= tolerance) }
150
+ }
151
+ private fun prepaid(price: JSONObject, quantity: BigDecimal): BigDecimal {
152
+ if (quantity.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO
153
+ var total = BigDecimal.ZERO; val pricing = o(price.opt("pricing"))
154
+ if (pricing.length() == 0) total = n(if (price.has("amountMinor")) price.opt("amountMinor") else price.opt("unitAmountMinor")) * quantity
155
+ else { var lower = BigDecimal.ZERO; for (row in a(pricing.opt("tiers"))) { val upper = if (row.isNull("upTo")) BigDecimal(10000) else n(row.opt("upTo")); if (pricing.getString("kind") == "volume" && quantity <= upper) { total = quantity * n(row.opt("unitAmountMinor")) + n(row.opt("flatAmountMinor")); break }; if (pricing.getString("kind") != "volume" && quantity > lower) total += (quantity.min(upper) - lower) * n(row.opt("unitAmountMinor")); lower = upper } }
156
+ require(total <= BigDecimal(200000000)); return total
157
+ }
158
+ private fun attachQuote(v: JSONObject) {
159
+ val lines = a(v.opt("lines")); val trial = o(lines.firstOrNull()?.opt("trial")); val execution = o(v.opt("execution")); val discount = o(v.opt("discount")); val provider = v.getString("provider")
160
+ if (provider == "stripe") {
161
+ if (trial.has("cardRequired") && !trial.getBoolean("cardRequired") && !o(execution.opt("invoiceMode")).optBoolean("enabled")) require(execution.length() == 0 && discount.length() == 0)
162
+ else {
163
+ require(execution.length() > 0 && o(execution.opt("metadata")).optString("grooveCheckoutId") == v.getString("operationId") && time(execution.opt("expiresAt")) <= time(v.opt("expiresAt")))
164
+ require(execution.getString("mode") == if (lines.any { it.getString("interval") !in listOf("one_time", "free") }) "subscription" else "payment")
165
+ require(execution.isNull("trialPeriodDays") == (trial.length() == 0) && (trial.length() == 0 || n(execution.opt("trialPeriodDays")).compareTo(n(trial.opt("days"))) == 0))
166
+ require(execution.getString("executor") == "indiana" || a(execution.opt("lineItems")).all { !it.isNull("price") })
167
+ val discounts = a(execution.opt("discounts")); require(discounts.size == if (discount.length() == 0) 0 else 1)
168
+ for (row in discounts) { val terms = o(row.opt("terms")); val coupon = row.getString("coupon"); require(coupon == terms.getString("id") && discount.length() > 0); if (coupon != discount.getString("couponId")) require(o(discount.opt("discount")).getString("type") == "amount" && coupon == discount.getString("couponId") + "_credit_" + discount.getString("offerId") + "_" + terms.get("amount_off").toString() && n(terms.opt("amount_off")).compareTo(n(o(discount.opt("discount")).opt("amountOffCents"))) == 0 && terms.optString("currency").uppercase() == v.getString("currency") && terms.getString("duration") == "once") }
169
+ }
170
+ require(!v.has("nativeOffer"))
171
+ } else { val selection = o(v.opt("nativeOffer")); require(execution.length() == 0 && selection.length() > 0 && lines.size == 1 && n(selection.opt("discountMinor")) <= n(v.opt("catalogAmountMinor")) && selection.isNull("trialDays") == (trial.length() == 0) && (trial.length() == 0 || n(selection.opt("trialDays")).compareTo(n(trial.opt("days"))) == 0) && (discount.length() == 0 || n(selection.opt("discountMinor")).compareTo(n(discount.opt("amountMinor"))) == 0)) }
172
+ if (discount.length() > 0) { val allocations = a(discount.opt("allocations")); require(v.isNull("billingSubscriptionId") && (provider == "stripe" || o(discount.opt("duration")).getString("type") == "once") && n(discount.opt("amountMinor")) <= n(v.opt("catalogAmountMinor")) && allocations.size == lines.size); for ((row, line) in allocations.zip(lines)) require(row.getString("productVersionId") == line.getString("productVersionId") && n(row.opt("amountMinor")) <= n(line.opt("catalogAmountMinor"))); require(allocations.fold(BigDecimal.ZERO) { total, row -> total + n(row.opt("amountMinor")) }.compareTo(n(discount.opt("amountMinor"))) == 0) }
173
+ if (!v.isNull("charge")) { val charge = v.getJSONObject("charge"); require(!v.isNull("billingSubscriptionId") && n(charge.opt("totalMinor")).compareTo(n(charge.opt("debitMinor")) - n(charge.opt("discountMinor")) + n(charge.opt("taxMinor"))) == 0 && n(charge.opt("discountMinor")) <= n(charge.opt("debitMinor")) && time(charge.opt("periodEndsAt")) > time(charge.opt("effectiveAt"))) }
174
+ require(unique(lines.map { it.getString("lineId") }) && lines.fold(BigDecimal.ZERO) { total, line -> total + n(line.opt("catalogAmountMinor")) }.compareTo(n(v.opt("catalogAmountMinor"))) == 0)
175
+ }
176
+ private fun offering(v: JSONObject) {
177
+ val products = mutableSetOf<String>(); val groups = mutableSetOf<String>(); var count = 0
178
+ fun visit(items: List<JSONObject>) { for (item in items) { count++; if (item.getString("type") == "product") { val id = item.getString("productId"); require(id == id.lowercase() && products.add(id)) } else { require(groups.add(item.getString("key"))); item.put("name", item.getString("name").trim()); visit(a(item.opt("items"))) } } }
179
+ visit(a(v.opt("items"))); require(products.size <= 20 && count <= 60)
180
+ val metadata = v.getJSONObject("metadata").toString(); require(metadata.codePointCount(0, metadata.length) <= 4000)
181
+ for (field in listOf("displayName", "lookupKey")) v.put(field, v.getString(field).trim())
182
+ }
183
+ private fun productGrouping(group: JSONObject): Set<String> = a(group.opt("assignments")).map { it.getString("productVersionId") }.toSet()
184
+ private fun screen(v: JSONObject) {
185
+ val pages = a(v.opt("pages")); val actions = a(v.opt("actions")); val strings = v.getJSONObject("strings"); val locales = strings.getJSONObject("locales").keys().asSequence().toSet(); val pageIds = pages.map { it.getString("id") }; val actionIds = actions.map { it.getString("id") }
186
+ require((v.getString("surface") == "paywall" || pages.size == 1) && pages.size <= 10 && actions.size <= 40 && locales.size <= 20 && strings.getString("defaultLocale") in locales && unique(pageIds) && unique(actionIds) && "next" !in pageIds && "back" !in pageIds)
187
+ require(v.isNull("initialPageId") || v.getString("initialPageId") in pageIds)
188
+ for (action in actions) if (action.getString("type") == "navigate") require(v.getString("surface") == "paywall" && action.getString("pageId") in pageIds)
189
+ val ids = mutableSetOf<String>(); var count = 0
190
+ fun visit(node: JSONObject, depth: Int) { require(depth <= 8 && ++count <= 150 && ids.add(node.getString("id"))); if (node.getString("type") == "button") require(node.getString("action") in actionIds); if (node.getString("type") == "custom_html") require(locales.containsAll(o(node.opt("html")).keys().asSequence().toSet())); if (node.getString("type") == "image") require(locales.containsAll(o(node.opt("localizedSrc")).keys().asSequence().toSet())); for (child in a(node.opt("children"))) visit(child, depth + 1) }
191
+ for (page in pages) visit(page.getJSONObject("root"), 1)
192
+ }
193
+ }