@tinkerlandai/indiana-react-native 0.9.1 → 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.
- package/IndianaReactNative.podspec +19 -0
- package/README.md +61 -4
- package/THIRD_PARTY_NOTICES.md +74 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/ai/tinkerland/indiana/reactnative/IndianaBridgeModule.kt +237 -0
- package/android/src/main/java/ai/tinkerland/indiana/reactnative/IndianaBridgePackage.kt +11 -0
- package/dist/index.cjs +17 -1
- package/dist/index.d.cts +57408 -604
- package/dist/index.d.ts +57408 -604
- package/dist/index.js +17 -1
- package/ios/IndianaBridge.m +11 -0
- package/ios/IndianaBridge.swift +275 -0
- package/native-sdk/android/assets/indiana/index.html +268 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/BillingAuthorizationTerms.kt +55 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/BundledWebView.kt +90 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/ContentSession.kt +45 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/ContentView.kt +36 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/CustomerOperations.generated.kt +84 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/CustomerResponseSemantics.kt +193 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/CustomerSemantics.kt +120 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/CustomerSession.kt +106 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/FeatureClient.kt +65 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/FunnelCommandStorage.kt +163 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/FunnelConfigurationRelay.kt +50 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/FunnelSession.kt +208 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/FunnelView.kt +71 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/GeneratedProtocol.kt +16 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/GoogleStorefront.kt +165 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/HTTPTransport.kt +43 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/IndianaClient.kt +526 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/NativePendingKeystore.kt +81 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/NativeReceiptRelay.kt +28 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/NativeStoreOffer.kt +24 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/PaywallContent.kt +75 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/PaywallSession.kt +144 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/PaywallView.kt +74 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/Protocol.kt +28 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/PushInstallation.kt +170 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/RendererNavigation.kt +13 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/SDKVersion.kt +6 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/StoreSubscriptionManagement.kt +19 -0
- package/native-sdk/android/java/ai/tinkerland/indiana/WireSchema.kt +76 -0
- package/native-sdk/android/resources/funnel-wire-schemas.json +1 -0
- package/native-sdk/android/resources/indiana-third-party-notices.txt +101 -0
- package/native-sdk/android/resources/renderer-data-url.txt +1 -0
- package/native-sdk/ios/AnalyticsReceipt.swift +10 -0
- package/native-sdk/ios/AppleStorefront.swift +218 -0
- package/native-sdk/ios/BillingAuthorizationTerms.swift +57 -0
- package/native-sdk/ios/BundledWebView.swift +87 -0
- package/native-sdk/ios/ContentSession.swift +48 -0
- package/native-sdk/ios/ContentView.swift +51 -0
- package/native-sdk/ios/CustomerOperations.generated.swift +166 -0
- package/native-sdk/ios/CustomerResponseSemantics.swift +232 -0
- package/native-sdk/ios/CustomerSemantics.swift +145 -0
- package/native-sdk/ios/CustomerSession.swift +129 -0
- package/native-sdk/ios/FeatureClient.swift +86 -0
- package/native-sdk/ios/FunnelCommandStorage.swift +163 -0
- package/native-sdk/ios/FunnelConfigurationRelay.swift +48 -0
- package/native-sdk/ios/FunnelSession.swift +232 -0
- package/native-sdk/ios/FunnelView.swift +125 -0
- package/native-sdk/ios/GeneratedProtocol.swift +16 -0
- package/native-sdk/ios/HTTPTransport.swift +74 -0
- package/native-sdk/ios/IndianaClient.swift +601 -0
- package/native-sdk/ios/NativePendingKeychain.swift +52 -0
- package/native-sdk/ios/NativeReceiptRelay.swift +28 -0
- package/native-sdk/ios/NativeStoreOffer.swift +34 -0
- package/native-sdk/ios/PaywallContent.swift +75 -0
- package/native-sdk/ios/PaywallSession.swift +124 -0
- package/native-sdk/ios/PaywallView.swift +87 -0
- package/native-sdk/ios/Protocol.swift +49 -0
- package/native-sdk/ios/PushInstallation.swift +154 -0
- package/native-sdk/ios/RendererNavigation.swift +8 -0
- package/native-sdk/ios/Resources.swift +15 -0
- package/native-sdk/ios/SDKVersion.swift +4 -0
- package/native-sdk/ios/StoreSubscriptionManagement.swift +11 -0
- package/native-sdk/ios/WebView/THIRD_PARTY_NOTICES.txt +101 -0
- package/native-sdk/ios/WebView/index.html +268 -0
- package/native-sdk/ios/WebView/renderer-data-url.txt +1 -0
- package/native-sdk/ios/WireSchema.swift +107 -0
- package/native-sdk/ios/funnel-wire-schemas.json +1 -0
- package/package.json +8 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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,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
|
+
}
|