@tinkerlandai/longplay-react-native 0.0.0-stage → 0.10.4

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 (87) hide show
  1. package/IndianaReactNative.podspec +19 -0
  2. package/README.md +119 -2
  3. package/THIRD_PARTY_NOTICES.md +101 -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 -0
  9. package/dist/index.d.cts +105286 -0
  10. package/dist/index.d.ts +105286 -0
  11. package/dist/index.js +17 -0
  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 +197 -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/MerchantCommerceSemantics.kt +38 -0
  33. package/native-sdk/android/java/ai/tinkerland/indiana/NativePendingKeystore.kt +81 -0
  34. package/native-sdk/android/java/ai/tinkerland/indiana/NativeReceiptRelay.kt +28 -0
  35. package/native-sdk/android/java/ai/tinkerland/indiana/NativeStoreOffer.kt +24 -0
  36. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallContent.kt +75 -0
  37. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallSession.kt +144 -0
  38. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallView.kt +74 -0
  39. package/native-sdk/android/java/ai/tinkerland/indiana/Protocol.kt +28 -0
  40. package/native-sdk/android/java/ai/tinkerland/indiana/PushInstallation.kt +170 -0
  41. package/native-sdk/android/java/ai/tinkerland/indiana/RendererNavigation.kt +13 -0
  42. package/native-sdk/android/java/ai/tinkerland/indiana/SDKVersion.kt +6 -0
  43. package/native-sdk/android/java/ai/tinkerland/indiana/StoreSubscriptionManagement.kt +19 -0
  44. package/native-sdk/android/java/ai/tinkerland/indiana/WireSchema.kt +76 -0
  45. package/native-sdk/android/java/ai/tinkerland/longplay/CustomerOperations.generated.kt +82 -0
  46. package/native-sdk/android/java/ai/tinkerland/longplay/Longplay.kt +32 -0
  47. package/native-sdk/android/resources/funnel-wire-schemas.json +1 -0
  48. package/native-sdk/android/resources/indiana-third-party-notices.txt +101 -0
  49. package/native-sdk/android/resources/renderer-data-url.txt +1 -0
  50. package/native-sdk/ios/AnalyticsReceipt.swift +10 -0
  51. package/native-sdk/ios/AppleStorefront.swift +218 -0
  52. package/native-sdk/ios/BillingAuthorizationTerms.swift +57 -0
  53. package/native-sdk/ios/BundledWebView.swift +87 -0
  54. package/native-sdk/ios/ContentSession.swift +48 -0
  55. package/native-sdk/ios/ContentView.swift +51 -0
  56. package/native-sdk/ios/CustomerOperations.generated.swift +166 -0
  57. package/native-sdk/ios/CustomerResponseSemantics.swift +236 -0
  58. package/native-sdk/ios/CustomerSemantics.swift +145 -0
  59. package/native-sdk/ios/CustomerSession.swift +129 -0
  60. package/native-sdk/ios/FeatureClient.swift +86 -0
  61. package/native-sdk/ios/FunnelCommandStorage.swift +163 -0
  62. package/native-sdk/ios/FunnelConfigurationRelay.swift +48 -0
  63. package/native-sdk/ios/FunnelSession.swift +232 -0
  64. package/native-sdk/ios/FunnelView.swift +125 -0
  65. package/native-sdk/ios/GeneratedProtocol.swift +16 -0
  66. package/native-sdk/ios/HTTPTransport.swift +74 -0
  67. package/native-sdk/ios/IndianaClient.swift +601 -0
  68. package/native-sdk/ios/MerchantCommerceSemantics.swift +51 -0
  69. package/native-sdk/ios/NativePendingKeychain.swift +52 -0
  70. package/native-sdk/ios/NativeReceiptRelay.swift +28 -0
  71. package/native-sdk/ios/NativeStoreOffer.swift +34 -0
  72. package/native-sdk/ios/PaywallContent.swift +75 -0
  73. package/native-sdk/ios/PaywallSession.swift +124 -0
  74. package/native-sdk/ios/PaywallView.swift +87 -0
  75. package/native-sdk/ios/Protocol.swift +49 -0
  76. package/native-sdk/ios/PushInstallation.swift +154 -0
  77. package/native-sdk/ios/RendererNavigation.swift +8 -0
  78. package/native-sdk/ios/Resources.swift +15 -0
  79. package/native-sdk/ios/SDKVersion.swift +4 -0
  80. package/native-sdk/ios/StoreSubscriptionManagement.swift +11 -0
  81. package/native-sdk/ios/WebView/THIRD_PARTY_NOTICES.txt +101 -0
  82. package/native-sdk/ios/WebView/index.html +268 -0
  83. package/native-sdk/ios/WebView/renderer-data-url.txt +1 -0
  84. package/native-sdk/ios/WireSchema.swift +107 -0
  85. package/native-sdk/ios/funnel-wire-schemas.json +1 -0
  86. package/package.json +62 -4
  87. 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://longplayai.com"
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,3 +1,120 @@
1
- # Temporary Holding Version
1
+ <p>
2
+ <a href="https://longplayai.com">
3
+ <img src="https://longplayai.com/brand/indiana-mark.png" alt="Longplay" width="72" height="72" />
4
+ </a>
5
+ </p>
2
6
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
7
+ # Longplay React Native SDK
8
+
9
+ This source checkout is SDK 0.10.4, protocol 0.10. 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.
10
+
11
+ 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.
12
+
13
+ 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.
14
+
15
+ Install the current release:
16
+
17
+ ```sh
18
+ npm install --save-exact @tinkerlandai/longplay-react-native@latest
19
+ ```
20
+
21
+ The first install resolves latest and saves an exact package.json version. Commit the lockfile and use npm ci for later builds. Use the original package identity and exact version from the [release records](../../docs/sdk-releases.md) for historical installations and rollback. New Longplay package names require their own publication; source changes do not publish artifacts.
22
+
23
+ ```ts
24
+ import { createLongplayReactNative } from "@tinkerlandai/longplay-react-native";
25
+
26
+ const longplay = createLongplayReactNative({
27
+ scope: { projectId: "YOUR_APP_UUID", environment: "sandbox" },
28
+ baseUrl: "https://api.longplayai.com",
29
+ usageReporter: async (command) => {
30
+ const response = await fetch("https://your-app.example/api/usage", {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ Authorization: `Bearer ${yourAppSessionToken}`,
35
+ },
36
+ body: JSON.stringify(command),
37
+ });
38
+ if (!response.ok) throw new Error("Usage reporting failed");
39
+ return response.json();
40
+ },
41
+ });
42
+
43
+ // Your backend issues a complete Access/Refresh pair for its authenticated user.
44
+ longplay.identify(session);
45
+ const holdings = await longplay.readHoldings();
46
+ const entitlement = await longplay.checkEntitlement({
47
+ requirement: {
48
+ internal_feature_id: "YOUR_FEATURE_UUID",
49
+ quantity: "1",
50
+ properties: {},
51
+ },
52
+ });
53
+ await longplay.trackUsage({
54
+ eventName: "images",
55
+ quantity: "1",
56
+ idempotencyKey: "stable-id-for-this-usage-event",
57
+ });
58
+ longplay.reset();
59
+ ```
60
+
61
+ Never ship a Server Key in the app. `usageReporter` receives only a validated usage command. Your backend authenticates its own app session, selects the Customer and invokes the server SDK with its Server Key; return that SDK's complete usage response unchanged. Preserve the event's idempotency key across retries. The SDK validates response schema and App/environment and rejects results if identity changes while reporting. The callback owns its network timeout and retry policy.
62
+
63
+ 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/longplay-sdk/react-native` subpath uses the same source implementation; install only one entry in an application.
64
+
65
+ ## Growth, marketing and delivery
66
+
67
+ 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.
68
+
69
+ 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.
70
+
71
+ ## Optional native presentation and purchases
72
+
73
+ 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.
74
+
75
+ 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:
76
+
77
+ ```groovy
78
+ android {
79
+ compileOptions {
80
+ coreLibraryDesugaringEnabled true
81
+ sourceCompatibility JavaVersion.VERSION_17
82
+ targetCompatibility JavaVersion.VERSION_17
83
+ }
84
+ }
85
+ dependencies {
86
+ coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5"
87
+ }
88
+ ```
89
+
90
+ ```ts
91
+ import { NativeEventEmitter, NativeModules } from "react-native";
92
+ const bridge = NativeModules.LongplayBridge;
93
+ const events = new NativeEventEmitter(bridge);
94
+ const longplay = createLongplayReactNative({
95
+ scope: { projectId, environment: "sandbox" },
96
+ baseUrl: "https://api.dev.longplayai.com",
97
+ nativeModule: bridge,
98
+ nativeEvents: (listener) => {
99
+ const subscription = events.addListener("LongplayAction", listener);
100
+ return () => subscription.remove();
101
+ },
102
+ usageReporter: sendToAuthenticatedBackend,
103
+ });
104
+ longplay.identify(customerSession); // JS owns refresh and supplies native Access Tokens.
105
+ await longplay.native.showFunnel(runToken);
106
+ await longplay.native.showPaywall({
107
+ paywallId,
108
+ provider: "apple",
109
+ currency: "USD",
110
+ });
111
+ await longplay.native.restorePurchases();
112
+ await longplay.native.manageSubscriptions(productId); // Product ID is required on Android.
113
+ await longplay.native.destroy();
114
+ ```
115
+
116
+ 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.
117
+
118
+ 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.
119
+
120
+ 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).
@@ -0,0 +1,101 @@
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
+
75
+ # whatwg-url-minimum 0.2.0
76
+
77
+ Bundled URL implementation from https://github.com/expo/whatwg-url-minimum.
78
+
79
+ MIT License
80
+
81
+ Copyright (c) Phil Pluckthun,
82
+ Copyright (c) 650 Industries, Inc. (aka Expo),
83
+ Copyright (c) Sebastian Mayr
84
+
85
+ Permission is hereby granted, free of charge, to any person obtaining a copy
86
+ of this software and associated documentation files (the "Software"), to deal
87
+ in the Software without restriction, including without limitation the rights
88
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
89
+ copies of the Software, and to permit persons to whom the Software is
90
+ furnished to do so, subject to the following conditions:
91
+
92
+ The above copyright notice and this permission notice shall be included in all
93
+ copies or substantial portions of the Software.
94
+
95
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
96
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
97
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
98
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
99
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
100
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
101
+ SOFTWARE.
@@ -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
+ }