@tinkerlandai/longplay-react-native 0.10.8 → 0.10.13
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/dist/index.cjs +2 -2
- package/dist/index.d.cts +26549 -12669
- package/dist/index.d.ts +26549 -12669
- package/dist/index.js +2 -2
- package/native-sdk/android/assets/indiana/index.html +42 -42
- package/native-sdk/android/java/ai/tinkerland/indiana/BundledWebView.kt +6 -2
- package/native-sdk/android/java/ai/tinkerland/indiana/CustomerSession.kt +9 -3
- package/native-sdk/android/java/ai/tinkerland/indiana/FeatureClient.kt +46 -22
- package/native-sdk/android/java/ai/tinkerland/indiana/FunnelConfigurationRelay.kt +47 -21
- package/native-sdk/android/java/ai/tinkerland/indiana/FunnelProcess.kt +120 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/FunnelSession.kt +57 -16
- package/native-sdk/android/java/ai/tinkerland/indiana/GoogleStorefront.kt +18 -2
- package/native-sdk/android/java/ai/tinkerland/indiana/HTTPTransport.kt +48 -20
- package/native-sdk/android/java/ai/tinkerland/indiana/IndianaClient.kt +36 -44
- package/native-sdk/android/java/ai/tinkerland/indiana/PaywallSession.kt +82 -59
- package/native-sdk/android/java/ai/tinkerland/indiana/SDKVersion.kt +1 -1
- package/native-sdk/android/java/ai/tinkerland/indiana/WireSchema.kt +3 -1
- package/native-sdk/android/java/ai/tinkerland/longplay/Longplay.kt +3 -0
- package/native-sdk/android/resources/funnel-wire-schemas.json +1 -1
- package/native-sdk/ios/BundledWebView.swift +5 -1
- package/native-sdk/ios/CustomerSession.swift +7 -3
- package/native-sdk/ios/FeatureClient.swift +75 -29
- package/native-sdk/ios/FunnelConfigurationRelay.swift +1 -0
- package/native-sdk/ios/FunnelProcess.swift +148 -0
- package/native-sdk/ios/FunnelSession.swift +37 -9
- package/native-sdk/ios/HTTPTransport.swift +2 -0
- package/native-sdk/ios/IndianaClient.swift +25 -60
- package/native-sdk/ios/NativeReceiptRelay.swift +3 -3
- package/native-sdk/ios/PaywallSession.swift +5 -4
- package/native-sdk/ios/SDKVersion.swift +1 -1
- package/native-sdk/ios/WebView/index.html +42 -42
- package/native-sdk/ios/funnel-wire-schemas.json +1 -1
- package/package.json +1 -1
- package/native-sdk/android/java/ai/tinkerland/indiana/NativePendingKeystore.kt +0 -81
- package/native-sdk/ios/NativePendingKeychain.swift +0 -52
|
@@ -52,7 +52,6 @@ class IndianaClient internal constructor(private val configuration: IndianaConfi
|
|
|
52
52
|
private val closed = AtomicBoolean(false)
|
|
53
53
|
private val storeRestorePending = java.util.concurrent.ConcurrentHashMap<String, Pair<UUID, String>>()
|
|
54
54
|
private val recoveringNative = AtomicBoolean(false)
|
|
55
|
-
private val nativeReceiptPending = java.util.concurrent.ConcurrentHashMap<UUID, Pair<UUID, String>>()
|
|
56
55
|
fun identify(customerToken: String) = synchronized(identityLock) {
|
|
57
56
|
if (!customerToken.matches(Regex("gg_ct_[A-Za-z0-9_-]{32,128}"))) throw IndianaClientException("unauthenticated")
|
|
58
57
|
installIdentity(customerToken, null)
|
|
@@ -63,7 +62,7 @@ class IndianaClient internal constructor(private val configuration: IndianaConfi
|
|
|
63
62
|
val next = Identity(customerToken, UUID.randomUUID())
|
|
64
63
|
val invalidated = { invalidateSession(next.epoch) }
|
|
65
64
|
if (session == null) http.identify(customerToken, invalidated) else http.identify(session, invalidated)
|
|
66
|
-
pushObservationID = UUID.randomUUID();
|
|
65
|
+
pushObservationID = UUID.randomUUID(); storeRestorePending.clear(); identity.set(next); funnels.forEach { it.get()?.reset() }; funnels.clear(); paywalls.forEach { it.get()?.close() }; paywalls.clear()
|
|
67
66
|
contents.forEach { it.get()?.close() }; contents.clear()
|
|
68
67
|
featuresClients.forEach { it.get()?.close() }; featuresClients.clear(); schedulePush()
|
|
69
68
|
}
|
|
@@ -83,7 +82,7 @@ class IndianaClient internal constructor(private val configuration: IndianaConfi
|
|
|
83
82
|
private fun clearIdentity() {
|
|
84
83
|
analyticsTimer?.cancel(); analyticsTimer = null; analyticsEvents.clear()
|
|
85
84
|
try { pushIdentity?.reserve(detach = true); pushSynchronizationError = null } catch (_: Exception) { pushSynchronizationError = "protected_storage_unavailable" }
|
|
86
|
-
pushObservationID = UUID.randomUUID(); pushBindingSequence = 0;
|
|
85
|
+
pushObservationID = UUID.randomUUID(); pushBindingSequence = 0; storeRestorePending.clear(); identity.set(Identity(null, UUID.randomUUID())); funnels.forEach { it.get()?.reset() }; funnels.clear(); paywalls.forEach { it.get()?.close() }; paywalls.clear()
|
|
87
86
|
contents.forEach { it.get()?.close() }; contents.clear()
|
|
88
87
|
featuresClients.forEach { it.get()?.close() }; featuresClients.clear(); schedulePush()
|
|
89
88
|
}
|
|
@@ -94,44 +93,16 @@ class IndianaClient internal constructor(private val configuration: IndianaConfi
|
|
|
94
93
|
http.identify(null)
|
|
95
94
|
clearIdentity()
|
|
96
95
|
}
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
checkOpen(); if (identity.get().epoch != actor.epoch) throw IndianaClientException("identity_changed")
|
|
108
|
-
val receipt = purchases.singleOrNull { it.productId == record.productId && it.accountBinding == record.accountId && it.intentBinding == record.profileId }
|
|
109
|
-
val command = nativeReceiptPending[record.orderId] ?: run {
|
|
110
|
-
val requestId = UUID.randomUUID()
|
|
111
|
-
if (receipt != null && receipt.purchaseToken.toByteArray().size > 16384) throw IndianaClientException("invalid_evidence")
|
|
112
|
-
val evidence = receipt?.let { JSONObject().put("provider", "google").put("orderId", record.orderId.toString()).put("receiptId", record.receiptId.toString()).put("purchaseToken", it.purchaseToken) }
|
|
113
|
-
requestId to JSONObject().put("operation", "native_receipt").put("requestId", requestId.toString()).put("customerToken", token).put("orderId", record.orderId.toString()).put("receiptId", record.receiptId.toString()).put("nativeEvidence", evidence ?: JSONObject.NULL).toString()
|
|
114
|
-
}.also { nativeReceiptPending[record.orderId] = it }
|
|
115
|
-
val credential = http.access()
|
|
116
|
-
if (identity.get().epoch != actor.epoch || closed.get()) throw IndianaClientException("identity_changed")
|
|
117
|
-
val outgoing = JSONObject(command.second).put("customerToken", credential)
|
|
118
|
-
val result = try { relay(outgoing) } catch (error: IndianaClientException) {
|
|
119
|
-
if (identity.get().epoch != actor.epoch || closed.get()) throw IndianaClientException("identity_changed")
|
|
120
|
-
val status = error.code.removePrefix("request_failed_").toIntOrNull()
|
|
121
|
-
if (status != null && status < 500 && status != 425) { nativeReceiptPending.remove(record.orderId); if (status in setOf(403,404)) continue }
|
|
122
|
-
throw error
|
|
123
|
-
}
|
|
124
|
-
if (identity.get().epoch != actor.epoch || closed.get()) throw IndianaClientException("identity_changed")
|
|
125
|
-
if (safe { scope(result.getJSONObject("scope")) } != configuration.scope || result.optString("requestId") != command.first.toString() || result.optString("orderId") != record.orderId.toString() || result.optString("receiptId") != record.receiptId.toString() || result.optString("orderStatus") !in setOf("created", "pending", "trialing", "paid", "cancelled", "failed") || result.opt("refunded") !is Boolean) throw IndianaClientException("invalid_response")
|
|
126
|
-
result.optJSONObject("nativeCompletion")?.let { completion ->
|
|
127
|
-
if (completion.optString("orderId") != record.orderId.toString() || completion.optString("receiptId") != record.receiptId.toString() || completion.opt("membership") !is Boolean) throw IndianaClientException("invalid_response")
|
|
128
|
-
val ids = completion.getJSONArray("transactionIds"); if (ids.length() !in 1..50 || (0 until ids.length()).any { ids.opt(it) !is String || ids.getString(it).length !in 1..255 } || (0 until ids.length()).map { ids.getString(it) }.distinct().size != ids.length()) throw IndianaClientException("invalid_response")
|
|
129
|
-
if (!completion.getBoolean("membership")) storage.remove(record.orderId)
|
|
130
|
-
}
|
|
131
|
-
nativeReceiptPending.remove(record.orderId); responses.add(JSONObject(result.toString()))
|
|
132
|
-
}
|
|
133
|
-
return responses
|
|
134
|
-
} finally { store.close(); recoveringNative.set(false) }
|
|
96
|
+
/**
|
|
97
|
+
* Play's purchase list is the recovery source: every SDK purchase carries its order binding and Play lists it until
|
|
98
|
+
* the server consumes it or the subscription ends, so [restorePurchases] and a retried [purchaseSubscription] recover it.
|
|
99
|
+
* No local order journal exists; this compatibility entry relays nothing and returns no orders.
|
|
100
|
+
*/
|
|
101
|
+
@Deprecated("Play's purchase list is the recovery source; call restorePurchases(context).")
|
|
102
|
+
@Suppress("UNUSED_PARAMETER")
|
|
103
|
+
suspend fun recoverNativeOrders(context: Context, relay: suspend (JSONObject) -> JSONObject): List<JSONObject> {
|
|
104
|
+
checkOpen(); if (identity.get().token == null) throw IndianaClientException("unauthenticated")
|
|
105
|
+
return emptyList()
|
|
135
106
|
}
|
|
136
107
|
/** Reinstall and device-change recovery uses the store's current transactions, never a local order journal. */
|
|
137
108
|
suspend fun restorePurchases(context: Context): List<JSONObject> = restoreStorePurchases(GoogleStorefront(context) {})
|
|
@@ -145,10 +116,24 @@ class IndianaClient internal constructor(private val configuration: IndianaConfi
|
|
|
145
116
|
if (identity.get().epoch != actor.epoch || closed.get()) throw IndianaClientException("identity_changed")
|
|
146
117
|
val intent = purchase.intentBinding?.let { runCatching { UUID.fromString(it) }.getOrNull() }
|
|
147
118
|
// Unbound historical purchases are not automatically transferred to a signed-in app account.
|
|
148
|
-
intent?.let {
|
|
119
|
+
intent?.let { restoreOne(purchase,it,actor.epoch) }
|
|
149
120
|
}
|
|
150
121
|
} finally { store.close(); recoveringNative.set(false) }
|
|
151
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The Play account can hold purchases bound to another Customer or scope. Their definite refusal (403/404) skips
|
|
125
|
+
* that purchase; any other refusal or unknown result ends the restore.
|
|
126
|
+
*/
|
|
127
|
+
private suspend fun restoreOne(purchase: GooglePurchaseEvidence, intent: UUID, original: UUID): JSONObject? = try {
|
|
128
|
+
submitStorePurchase(purchase,intent,original)
|
|
129
|
+
} catch (error: IndianaClientException) {
|
|
130
|
+
if (identity.get().epoch != original || closed.get()) throw IndianaClientException("identity_changed")
|
|
131
|
+
val status = error.code.removePrefix("request_failed_").toIntOrNull()
|
|
132
|
+
// A definite refusal settles that request; a later attempt starts a new one.
|
|
133
|
+
if (status != null && status < 500 && status != 425) storeRestorePending.remove(purchase.purchaseToken)
|
|
134
|
+
if (status != 403 && status != 404) throw error
|
|
135
|
+
null
|
|
136
|
+
}
|
|
152
137
|
private suspend fun submitStorePurchase(purchase: GooglePurchaseEvidence, intent: UUID, original: UUID): JSONObject {
|
|
153
138
|
if (purchase.purchaseToken.toByteArray().size > 16384) throw IndianaClientException("invalid_evidence")
|
|
154
139
|
val command = storeRestorePending[purchase.purchaseToken] ?: run {
|
|
@@ -165,7 +150,12 @@ class IndianaClient internal constructor(private val configuration: IndianaConfi
|
|
|
165
150
|
}
|
|
166
151
|
storeRestorePending.remove(purchase.purchaseToken); return result
|
|
167
152
|
}
|
|
168
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* Uses the fixed server instruction and actual Play quote; the server owns acknowledgement and grants. A dismissed
|
|
155
|
+
* sheet or a payment Play refused ends the operation as cancelled. The wait for Play's result follows the user's
|
|
156
|
+
* sheet and has no deadline: cancel the calling coroutine to stop waiting. The operation then stays pending, and a
|
|
157
|
+
* retry with the same `operationId` first submits any purchase Play completed.
|
|
158
|
+
*/
|
|
169
159
|
suspend fun purchaseSubscription(activity: Activity, operationId: UUID, confirm: suspend (GoogleQuote) -> Boolean): JSONObject {
|
|
170
160
|
if (!recoveringNative.compareAndSet(false,true)) throw IndianaClientException("command_pending")
|
|
171
161
|
try { return purchaseSubscriptionInternal(activity,operationId,false,confirm) } finally { recoveringNative.set(false) }
|
|
@@ -330,7 +320,9 @@ class IndianaClient internal constructor(private val configuration: IndianaConfi
|
|
|
330
320
|
/** The authenticated merchant backend selects the Customer and adds its Server Key; retain idempotencyKey on retry. */
|
|
331
321
|
suspend fun trackUsage(command: JSONObject, relay: suspend (JSONObject) -> JSONObject): JSONObject {
|
|
332
322
|
checkOpen(); val actor = identity.get(); if (actor.token == null) throw IndianaClientException("unauthenticated")
|
|
333
|
-
val body = JSONObject(command.toString())
|
|
323
|
+
val body = JSONObject(command.toString())
|
|
324
|
+
try { IndianaSchema.validate("usageCommand", body) }
|
|
325
|
+
catch (_: Exception) { throw IndianaClientException("invalid_request") }
|
|
334
326
|
val result = try { relay(body) } catch (error: Exception) {
|
|
335
327
|
if (identity.get().epoch != actor.epoch || closed.get()) throw IndianaClientException("identity_changed")
|
|
336
328
|
throw error
|
|
@@ -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() {
|
|
33
|
-
|
|
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
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
result.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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")
|
|
@@ -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
|