@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,19 @@
1
+ require "json"
2
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
3
+ Pod::Spec.new do |s|
4
+ s.name = "IndianaReactNative"
5
+ s.version = package["version"]
6
+ s.summary = "Indiana Customer SDK native presentation and StoreKit bridge"
7
+ s.homepage = "https://tinker.cash"
8
+ s.license = { :type => "Proprietary" }
9
+ s.author = "Tinkerland"
10
+ s.source = { :git => "https://github.com/tinkerlandai/Indiana.git", :tag => "sdk-v#{s.version}" }
11
+ s.platform = :ios, "16.0"
12
+ s.swift_version = "6.0"
13
+ s.source_files = "ios/**/*.{swift,h,m}", "native-sdk/ios/**/*.swift"
14
+ s.resource_bundles = {
15
+ "IndianaResources" => ["native-sdk/ios/funnel-wire-schemas.json", "native-sdk/ios/WebView"]
16
+ }
17
+ s.frameworks = "StoreKit", "WebKit", "UIKit", "JavaScriptCore"
18
+ s.dependency "React-Core"
19
+ end
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # Indiana React Native SDK
2
2
 
3
- These examples target SDK 0.9.1 and protocol 0.9 at `https://api.tinker.cash`, using a Sandbox App and credentials issued by the production deployment. Sandbox still contains production data; do not reuse credentials from the separate test deployment. Confirm runtime readiness in [environment state](../../docs/development-status.md) before running them.
3
+ This source checkout is SDK 0.9.5, protocol 0.9. Publication and runtime promotion are separate; check the [release ledger](../../docs/sdk-releases.md) and [environment state](../../docs/development-status.md) before selecting an install or API target. New capabilities below require this source candidate or its verified publication. Set the API origin explicitly: the SDK default is production.
4
4
 
5
- Customer identity, holdings, entitlement checks and backend-mediated usage reporting for React Native 0.79+ / Metro 0.82+. This independent npm package uses Fetch and AbortController. It needs no DOM, WebView, native linking or React peer dependency. It does not include store purchases or Funnel native UI.
5
+ React Native 0.79+ / Metro 0.82+ uses the shared portable Customer facade: identity/session refresh, holdings, entitlement checks, commercial commands, Coupons/Rewards/Referrals, Offering/Paywall/content resolution, experimentation, Analytics, Messaging and Push adapters. Funnel and independent Paywall data sessions share that identity. Trusted usage writes still go through the merchant backend.
6
6
 
7
- Install the exact package version:
7
+ The data client needs Fetch and AbortController, with no DOM or native linking. Optional native integration autolinks the included Swift/Kotlin SDK sources and packaged renderer, adding Funnel/Paywall/content presentation and Apple/Google purchase, restore and subscription-management bridges. It never carries a Server Key.
8
+
9
+ The recorded public baseline remains:
8
10
 
9
11
  ```sh
10
12
  npm install --save-exact @tinkerlandai/indiana-react-native@0.9.1
@@ -52,4 +54,59 @@ Never ship a Server Key in the app. `usageReporter` receives only a validated us
52
54
 
53
55
  Credentials stay in memory. Passing the complete session pair enables automatic refresh shared by concurrent requests; a bare Access Token requires host-managed renewal. Successful rotation preserves identity; a definitive refresh refusal or expired Refresh Token clears it and stops usage reporting until a new verified login. Call `reset()` at logout. Customer creation and token issuance belong on your backend. The existing `@tinkerlandai/indiana-sdk/react-native` subpath uses the same source implementation; install only one entry in an application.
54
56
 
55
- Package versions use the shared `sdk-vX.Y.Z` release. The package includes ESM, CommonJS and TypeScript declarations and a portable URL parser for React Native hosts whose built-in URL is incomplete. It does not replace application globals; its only runtime dependency is Zod. See the repository's [mobile integration guide](https://github.com/tinkerlandai/Indiana/blob/main/docs/mobile-sdk.md) and [release commands](https://github.com/tinkerlandai/Indiana/blob/main/docs/automated-releases.md).
57
+ ## Growth, marketing and delivery
58
+
59
+ The portable client uses the same request/response contracts as the browser Customer client. Use `features(...)` for experimentation, `track(...)` and `flush()` for marketing events, and `listOffers()` / `claimCoupon(...)` for independent Coupons. Marketing events remain separate from trusted usage writes.
60
+
61
+ Standalone data sessions do not open a UI. `funnel().start(...)` starts under the current Customer; `paywall(...)` prepares an independent Paywall session; `resumeFunnel(runToken)` restores its Run controller. Retain the original request ID for unknown results. Non-browser controllers require a protected `funnelCommandStorage` CAS adapter for durable command recovery. Configuration persistence accepts an explicit `ConfigurationStorage` adapter.
62
+
63
+ ## Optional native presentation and purchases
64
+
65
+ Build/install this source candidate's package, then run your app's normal CocoaPods installation and native build. iOS requires 16+; the Android native adapter requires API 24+ and the normal Play Billing setup. Native sources and renderer resources are included in the npm package.
66
+
67
+ Enable core-library API desugaring in the Android application's `build.gradle` for Customer session `java.time` support on API 24–25. The adapter enables the same setting in its library module:
68
+
69
+ ```groovy
70
+ android {
71
+ compileOptions {
72
+ coreLibraryDesugaringEnabled true
73
+ sourceCompatibility JavaVersion.VERSION_17
74
+ targetCompatibility JavaVersion.VERSION_17
75
+ }
76
+ }
77
+ dependencies {
78
+ coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5"
79
+ }
80
+ ```
81
+
82
+ ```ts
83
+ import { NativeEventEmitter, NativeModules } from "react-native";
84
+ const bridge = NativeModules.IndianaBridge;
85
+ const events = new NativeEventEmitter(bridge);
86
+ const indiana = createIndianaReactNative({
87
+ scope: { projectId, environment: "sandbox" },
88
+ baseUrl: "https://api.dev.tinker.cash",
89
+ nativeModule: bridge,
90
+ nativeEvents: (listener) => {
91
+ const subscription = events.addListener("IndianaAction", listener);
92
+ return () => subscription.remove();
93
+ },
94
+ usageReporter: sendToAuthenticatedBackend,
95
+ });
96
+ indiana.identify(customerSession); // JS owns refresh and supplies native Access Tokens.
97
+ await indiana.native.showFunnel(runToken);
98
+ await indiana.native.showPaywall({
99
+ paywallId,
100
+ provider: "apple",
101
+ currency: "USD",
102
+ });
103
+ await indiana.native.restorePurchases();
104
+ await indiana.native.manageSubscriptions(productId); // Product ID is required on Android.
105
+ await indiana.native.destroy();
106
+ ```
107
+
108
+ Each JS client owns an independent native instance. Logout/account changes close its presentation and invalidate pending results. Direct `purchaseSubscription(operationId, true)` and `switchSubscription(operationId, true)` require explicit purchase acceptance and use native provider verification. Keep the original commercial operation ID for recovery; closing a store sheet does not prove purchase completion.
109
+
110
+ The JS session is the only Refresh Token owner. Keep `nativeEvents` subscribed while using native capabilities; the bridge requests a current Access Token for each authenticated native request and never receives a Refresh Token. Rotation preserves open native surfaces. An externally managed Access Token can be used without `nativeEvents`; a full Customer session requires the event subscription.
111
+
112
+ Package versions use the shared `sdk-vX.Y.Z` release. The package includes ESM, CommonJS and TypeScript declarations, native source/resources, autolinking configuration and a portable URL parser. It does not replace application globals; the JS entry's only runtime package dependency is Zod. Native presentation adds the platform frameworks and React Native bridge dependencies. See the repository's [mobile integration guide](https://github.com/tinkerlandai/Indiana/blob/main/docs/mobile-sdk.md) and [release commands](https://github.com/tinkerlandai/Indiana/blob/main/docs/automated-releases.md).
@@ -1,3 +1,77 @@
1
+ # GrowthBook core
2
+
3
+ MIT core port from growthbook/growthbook at ed29827a453ae6b9eba181bdf4e9280d4b670817. No enterprise-licensed code is included.
4
+
5
+ Copyright (c) 2025 GrowthBook, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
20
+
21
+ # @noble/hashes 2.4.0
22
+
23
+ SHA-256 for hashed saved-group membership is bundled from `@noble/hashes` 2.4.0.
24
+ Source: https://github.com/paulmillr/noble-hashes
25
+
26
+ The MIT License (MIT)
27
+
28
+ Copyright (c) 2022 Paul Miller (https://paulmillr.com)
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ of this software and associated documentation files (the “Software”), to deal
32
+ in the Software without restriction, including without limitation the rights
33
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ copies of the Software, and to permit persons to whom the Software is
35
+ furnished to do so, subject to the following conditions:
36
+
37
+ The above copyright notice and this permission notice shall be included in
38
+ all copies or substantial portions of the Software.
39
+
40
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
46
+ THE SOFTWARE.
47
+
48
+ # dom-mutator 0.7.0
49
+
50
+ GrowthBook declarative DOM mutations use the original `dom-mutator` package.
51
+ Source: https://github.com/growthbook/dom-mutator
52
+
53
+ MIT License
54
+
55
+ Copyright (c) 2021 Jeremy Dorn
56
+
57
+ Permission is hereby granted, free of charge, to any person obtaining a copy
58
+ of this software and associated documentation files (the "Software"), to deal
59
+ in the Software without restriction, including without limitation the rights
60
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
61
+ copies of the Software, and to permit persons to whom the Software is
62
+ furnished to do so, subject to the following conditions:
63
+
64
+ The above copyright notice and this permission notice shall be included in all
65
+ copies or substantial portions of the Software.
66
+
67
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
68
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
69
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
70
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
71
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
72
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
73
+ SOFTWARE.
74
+
1
75
  # whatwg-url-minimum 0.2.0
2
76
 
3
77
  Bundled URL implementation from https://github.com/expo/whatwg-url-minimum.
@@ -0,0 +1,24 @@
1
+ apply plugin: "com.android.library"
2
+ apply plugin: "org.jetbrains.kotlin.android"
3
+
4
+ android {
5
+ namespace "ai.tinkerland.indiana.reactnative"
6
+ compileSdkVersion rootProject.ext.has("compileSdkVersion") ? rootProject.ext.compileSdkVersion : 36
7
+ defaultConfig { minSdkVersion 24 }
8
+ sourceSets {
9
+ main.java.srcDirs += ["../native-sdk/android/java"]
10
+ main.assets.srcDirs += ["../native-sdk/android/assets"]
11
+ main.resources.srcDirs += ["../native-sdk/android/resources"]
12
+ }
13
+ compileOptions {
14
+ coreLibraryDesugaringEnabled true
15
+ sourceCompatibility JavaVersion.VERSION_17
16
+ targetCompatibility JavaVersion.VERSION_17
17
+ }
18
+ }
19
+ dependencies {
20
+ coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5"
21
+ implementation "com.facebook.react:react-android"
22
+ implementation "com.android.billingclient:billing:9.1.0"
23
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
24
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
3
+ </manifest>
@@ -0,0 +1,237 @@
1
+ package ai.tinkerland.indiana.reactnative
2
+
3
+ import ai.tinkerland.indiana.*
4
+ import android.app.Dialog
5
+ import android.view.View
6
+ import com.facebook.react.bridge.*
7
+ import com.facebook.react.modules.core.DeviceEventManagerModule
8
+ import kotlinx.coroutines.*
9
+ import org.json.JSONObject
10
+ import java.util.UUID
11
+
12
+ /** Scoped Customer instances, using the native SDK's packaged renderer and store verification. */
13
+ class IndianaBridgeModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
14
+ private class Instance(val client: IndianaClient) {
15
+ var revision = UUID.randomUUID()
16
+ var identitySequence = -1L
17
+ val actions = mutableMapOf<String, CompletableDeferred<Unit>>()
18
+ val credentials = mutableMapOf<String, CompletableDeferred<String>>()
19
+ var dismiss: (() -> Unit)? = null
20
+ var presentation: UUID? = null
21
+ }
22
+ private val instances = mutableMapOf<String, Instance>()
23
+ private val jobs = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
24
+ override fun getName() = "IndianaBridge"
25
+ private fun fields(input: String): JSONObject {
26
+ require(input.toByteArray(Charsets.UTF_8).size <= 2_097_152)
27
+ return JSONObject(input)
28
+ }
29
+ private fun instance(id: String) = instances[id] ?: throw IllegalArgumentException("Unknown Indiana instance")
30
+ private fun cancelActions(owner: Instance) {
31
+ owner.actions.values.toList().forEach { it.completeExceptionally(IllegalStateException("Customer identity changed")) }
32
+ owner.actions.clear()
33
+ owner.credentials.values.toList().forEach { it.completeExceptionally(IllegalStateException("Customer identity changed")) }
34
+ owner.credentials.clear()
35
+ }
36
+ private suspend fun credential(id: String): String = withContext(Dispatchers.Main.immediate) {
37
+ val owner = instance(id); val request = UUID.randomUUID().toString()
38
+ val result = CompletableDeferred<String>(); owner.credentials[request] = result
39
+ try {
40
+ context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(
41
+ "IndianaAction", Arguments.makeNativeMap(mapOf("instanceId" to id, "requestId" to request, "revision" to owner.identitySequence.toDouble(), "kind" to "credential"))
42
+ )
43
+ withTimeout(45_000) { result.await() }
44
+ } finally { owner.credentials.remove(request) }
45
+ }
46
+ @ReactMethod fun provideCredential(id: String, requestId: String, token: String?, promise: Promise) = run(promise) {
47
+ instance(id).credentials.remove(requestId)?.let {
48
+ if (token != null) it.complete(token) else it.completeExceptionally(IllegalStateException("Customer authentication required"))
49
+ }
50
+ null
51
+ }
52
+ private fun nativeJson(value: Any?): Any? = when (value) {
53
+ null, JSONObject.NULL -> null
54
+ is JSONObject -> value.keys().asSequence().associateWith { nativeJson(value.get(it)) }
55
+ is org.json.JSONArray -> (0 until value.length()).map { nativeJson(value.get(it)) }
56
+ else -> value
57
+ }
58
+ private suspend fun action(owner: Instance, revision: UUID, payload: JSONObject) {
59
+ check(owner.revision == revision) { "Customer identity changed" }
60
+ val id = instances.entries.single { it.value === owner }.key
61
+ val request = UUID.randomUUID().toString()
62
+ val result = CompletableDeferred<Unit>()
63
+ owner.actions[request] = result
64
+ try {
65
+ context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(
66
+ "IndianaAction", Arguments.makeNativeMap(mapOf("instanceId" to id, "requestId" to request, "revision" to owner.identitySequence.toDouble(), "action" to nativeJson(payload)))
67
+ )
68
+ withTimeout(45_000) { result.await() }
69
+ check(owner.revision == revision) { "Customer identity changed" }
70
+ } finally { owner.actions.remove(request) }
71
+ }
72
+ @ReactMethod fun addListener(name: String) { }
73
+ @ReactMethod fun removeListeners(count: Double) { }
74
+ @ReactMethod fun acknowledge(id: String, requestId: String, accepted: Boolean, promise: Promise) = run(promise) {
75
+ instance(id).actions.remove(requestId)?.let {
76
+ if (accepted) it.complete(Unit)
77
+ else it.completeExceptionally(IllegalStateException("Host action refused"))
78
+ }
79
+ null
80
+ }
81
+ private fun run(promise: Promise, action: suspend () -> Any?) {
82
+ jobs.launch {
83
+ try { promise.resolve(action()) }
84
+ catch (error: CancellationException) { promise.reject("INDIANA_NATIVE_CANCELLED", "Native operation cancelled") }
85
+ catch (error: Exception) { promise.reject("INDIANA_NATIVE_ERROR", error.javaClass.simpleName) }
86
+ }
87
+ }
88
+
89
+ @ReactMethod fun create(configuration: String, promise: Promise) = run(promise) {
90
+ val input = fields(configuration)
91
+ val scope = input.getJSONObject("scope")
92
+ val environment = IndianaEnvironment.entries.single { it.value == scope.getString("environment") }
93
+ val config = IndianaConfiguration(input.getString("baseUrl"), IndianaScope(UUID.fromString(scope.getString("projectId")), environment), (input.getDouble("timeout") * 1000).toInt())
94
+ val id = UUID.randomUUID().toString()
95
+ instances[id] = Instance(IndianaClient(context, config))
96
+ if (input.optBoolean("credentialEvents")) instances[id]!!.client.setAccessTokenProvider { credential(id) }
97
+ id
98
+ }
99
+
100
+ @ReactMethod fun identify(id: String, session: String, promise: Promise) = run(promise) {
101
+ require(session.toByteArray(Charsets.UTF_8).size <= 32768)
102
+ val owner = instance(id)
103
+ val frame = fields(session)
104
+ val rawSequence = frame.getDouble("revision")
105
+ require(rawSequence.isFinite() && rawSequence >= 0 && rawSequence <= 9007199254740991.0 && rawSequence % 1.0 == 0.0)
106
+ val sequence = rawSequence.toLong()
107
+ require(sequence >= 0 && sequence >= owner.identitySequence)
108
+ owner.identitySequence = sequence
109
+ owner.revision = UUID.randomUUID(); owner.dismiss?.invoke(); owner.dismiss = null; owner.presentation = null; cancelActions(owner)
110
+ owner.client.identify(frame.getString("credentials"))
111
+ null
112
+ }
113
+ @ReactMethod fun reset(id: String, revision: Double, promise: Promise) = run(promise) {
114
+ val owner = instance(id)
115
+ require(revision.isFinite() && revision >= 0 && revision % 1.0 == 0.0)
116
+ if (revision.toLong() >= owner.identitySequence) {
117
+ owner.identitySequence = revision.toLong()
118
+ owner.revision = UUID.randomUUID(); owner.dismiss?.invoke(); owner.dismiss = null; owner.presentation = null; cancelActions(owner)
119
+ owner.client.reset()
120
+ }
121
+ null
122
+ }
123
+ @ReactMethod fun destroy(id: String, promise: Promise) = run(promise) {
124
+ instances.remove(id)?.let { owner ->
125
+ owner.revision = UUID.randomUUID(); owner.dismiss?.invoke(); cancelActions(owner); owner.client.close()
126
+ }
127
+ null
128
+ }
129
+ @ReactMethod fun invoke(id: String, method: String, input: String, promise: Promise) = run(promise) {
130
+ val owner = instance(id)
131
+ val original = owner.revision
132
+ val value = fields(input)
133
+ val activity = context.currentActivity
134
+ val result = when (method) {
135
+ "restorePurchases" -> org.json.JSONArray(owner.client.restorePurchases(context)).toString()
136
+ "purchaseSubscription", "switchSubscription" -> {
137
+ require(value.getBoolean("accepted"))
138
+ val host = activity ?: throw IllegalStateException("Foreground Activity required")
139
+ val operation = UUID.fromString(value.getString("operationId"))
140
+ (if (method == "purchaseSubscription") owner.client.purchaseSubscription(host, operation) { true }
141
+ else owner.client.switchSubscription(host, operation) { true }).toString()
142
+ }
143
+ "manageSubscriptions" -> {
144
+ IndianaStoreSubscriptions.manage(activity ?: throw IllegalStateException("Foreground Activity required"), value.getString("productId"))
145
+ "null"
146
+ }
147
+ "showFunnel", "showPaywall", "showContent" -> present(owner, original, method, value)
148
+ else -> throw IllegalArgumentException("Unsupported native operation")
149
+ }
150
+ check(original == owner.revision) { "Customer identity changed" }
151
+ result
152
+ }
153
+
154
+ private suspend fun present(owner: Instance, revision: UUID, method: String, input: JSONObject): String {
155
+ check(owner.presentation == null) { "A native surface is already open" }
156
+ val activity = context.currentActivity ?: throw IllegalStateException("Foreground Activity required")
157
+ val reservation = UUID.randomUUID()
158
+ owner.presentation = reservation
159
+ val dialog = Dialog(activity)
160
+ val result = CompletableDeferred<String>()
161
+ var cleanup: (() -> Unit)? = null
162
+ fun finish(value: JSONObject) {
163
+ if (result.complete(value.toString())) {
164
+ if (owner.presentation == reservation) { owner.dismiss = null; owner.presentation = null }
165
+ dialog.dismiss()
166
+ cleanup?.invoke()
167
+ }
168
+ }
169
+ fun failed(error: Throwable) {
170
+ if (result.completeExceptionally(error)) {
171
+ if (owner.presentation == reservation) { owner.dismiss = null; owner.presentation = null }
172
+ dialog.dismiss()
173
+ cleanup?.invoke()
174
+ }
175
+ }
176
+ val dismissed = { finish(JSONObject().put("status", "dismissed")) }
177
+ val purchase: suspend (JSONObject) -> Unit = { operation ->
178
+ check(owner.revision == revision) { "Customer identity changed" }
179
+ owner.client.purchaseSubscription(activity, UUID.fromString(operation.getString("operationId"))) { true }
180
+ Unit
181
+ }
182
+ return try {
183
+ val view: View = when (method) {
184
+ "showFunnel" -> {
185
+ val session = owner.client.funnel(input.getString("runToken"))
186
+ IndianaFunnelView(activity, session).also {
187
+ cleanup = it::destroy
188
+ it.onPurchase = purchase; it.onDismiss = dismissed; it.onError = ::failed
189
+ it.onEvent = { name -> action(owner, revision, JSONObject().put("type", "event").put("name", name)) }
190
+ it.onComplete = { run -> finish(JSONObject().put("status", "complete").put("result", run)) }
191
+ }
192
+ }
193
+ "showPaywall" -> {
194
+ val provider = IndianaSubscriptionProvider.entries.single { it.value == input.getString("provider") }
195
+ val session = owner.client.paywall(
196
+ paywallId = input.optString("paywallId").takeIf(String::isNotEmpty)?.let(UUID::fromString),
197
+ paywallVersionId = input.optString("paywallVersionId").takeIf(String::isNotEmpty)?.let(UUID::fromString),
198
+ provider = provider, currency = input.getString("currency"), locale = input.optString("locale", "en")
199
+ )
200
+ IndianaPaywallView(activity, session).also {
201
+ cleanup = it::destroy
202
+ it.onPurchase = purchase; it.onDismiss = dismissed; it.onError = ::failed
203
+ it.onEvent = { name -> action(owner, revision, JSONObject().put("type", "event").put("name", name)) }
204
+ it.onComplete = { operation -> finish(JSONObject().put("status", "complete").put("result", operation)) }
205
+ }
206
+ }
207
+ else -> {
208
+ val session = owner.client.content(JSONObject().put("kind", input.get("kind")).put("selector", input.get("selector")), input.optString("locale", "en"), input.optJSONObject("inputs") ?: JSONObject())
209
+ IndianaContentView(activity, session).also {
210
+ cleanup = it::destroy
211
+ it.onDismiss = dismissed; it.onError = ::failed
212
+ it.onAction = { payload -> action(owner, revision, payload) }
213
+ }
214
+ }
215
+ }
216
+ check(owner.revision == revision && owner.presentation == reservation) { "Customer identity changed" }
217
+ owner.dismiss = dismissed
218
+ dialog.setContentView(view)
219
+ dialog.setOnCancelListener { dismissed() }
220
+ dialog.show()
221
+ result.await()
222
+ }
223
+ finally {
224
+ if (owner.presentation == reservation) { owner.dismiss = null; owner.presentation = null }
225
+ dialog.dismiss()
226
+ cleanup?.invoke()
227
+ }
228
+ }
229
+ override fun invalidate() {
230
+ jobs.launch {
231
+ instances.values.forEach { it.dismiss?.invoke(); cancelActions(it); it.client.close() }
232
+ instances.clear()
233
+ jobs.cancel()
234
+ }
235
+ super.invalidate()
236
+ }
237
+ }
@@ -0,0 +1,11 @@
1
+ package ai.tinkerland.indiana.reactnative
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class IndianaBridgePackage : ReactPackage {
9
+ override fun createNativeModules(context: ReactApplicationContext): List<NativeModule> = listOf(IndianaBridgeModule(context))
10
+ override fun createViewManagers(context: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
11
+ }