@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,11 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
3
+ @interface RCT_EXTERN_MODULE(IndianaBridge, RCTEventEmitter)
4
+ RCT_EXTERN_METHOD(create:(NSString *)configuration resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
5
+ RCT_EXTERN_METHOD(identify:(NSString *)instanceId session:(NSString *)session resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
6
+ RCT_EXTERN_METHOD(invoke:(NSString *)instanceId method:(NSString *)method input:(NSString *)input resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
7
+ RCT_EXTERN_METHOD(reset:(NSString *)instanceId revision:(nonnull NSNumber *)revision resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
8
+ RCT_EXTERN_METHOD(destroy:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
9
+ RCT_EXTERN_METHOD(acknowledge:(NSString *)instanceId requestId:(NSString *)requestId accepted:(BOOL)accepted resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
10
+ RCT_EXTERN_METHOD(provideCredential:(NSString *)instanceId requestId:(NSString *)requestId token:(NSString * _Nullable)token resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
11
+ @end
@@ -0,0 +1,275 @@
1
+ import Foundation
2
+ @preconcurrency import React
3
+ import UIKit
4
+
5
+ /// One scoped Customer client per JS SDK instance. No Server Key crosses this bridge.
6
+ @MainActor @objc(IndianaBridge)
7
+ final class IndianaBridge: RCTEventEmitter {
8
+ private struct Configuration: Decodable {
9
+ let baseUrl: String
10
+ let scope: IndianaScope
11
+ let timeout: Double
12
+ let credentialEvents: Bool?
13
+ }
14
+ @MainActor private final class Instance {
15
+ let client: IndianaClient
16
+ var revision = UUID()
17
+ var identitySequence = -1
18
+ var actions: [String: CheckedContinuation<Void, Error>] = [:]
19
+ var credentials: [String: CheckedContinuation<String, Error>] = [:]
20
+ var close: (() -> Void)?
21
+ var presentation: UUID?
22
+ init(_ client: IndianaClient) { self.client = client }
23
+ }
24
+ private var instances: [String: Instance] = [:]
25
+ @objc nonisolated override static func requiresMainQueueSetup() -> Bool { true }
26
+ @objc nonisolated override var methodQueue: DispatchQueue { .main }
27
+ nonisolated override func supportedEvents() -> [String]! { ["IndianaAction"] }
28
+ nonisolated override func invalidate() {
29
+ Task { @MainActor in
30
+ let owned = instances.values; instances.removeAll()
31
+ for value in owned {
32
+ value.revision = UUID(); value.close?(); cancelActions(value)
33
+ await value.client.reset()
34
+ }
35
+ }
36
+ super.invalidate()
37
+ }
38
+ private func cancelActions(_ owner: Instance) {
39
+ let pending = owner.actions.values; owner.actions.removeAll()
40
+ pending.forEach { $0.resume(throwing: IndianaClientError.identityChanged) }
41
+ let credentials = owner.credentials.values; owner.credentials.removeAll()
42
+ credentials.forEach { $0.resume(throwing: IndianaClientError.identityChanged) }
43
+ }
44
+ private func credential(_ id: String) async throws -> String {
45
+ let owner = try instance(id), request = UUID().uuidString
46
+ return try await withCheckedThrowingContinuation { continuation in
47
+ owner.credentials[request] = continuation
48
+ sendEvent(withName: "IndianaAction", body: ["instanceId": id, "requestId": request, "revision": owner.identitySequence, "kind": "credential"])
49
+ Task { @MainActor in
50
+ try? await Task.sleep(for: .seconds(45))
51
+ owner.credentials.removeValue(forKey: request)?.resume(throwing: IndianaClientError.temporaryUnavailable)
52
+ }
53
+ }
54
+ }
55
+ @objc(provideCredential:requestId:token:resolver:rejecter:)
56
+ func provideCredential(_ id: String, requestId: String, token: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
57
+ do {
58
+ let owner = try instance(id)
59
+ if let continuation = owner.credentials.removeValue(forKey: requestId) {
60
+ if let token { continuation.resume(returning: token) }
61
+ else { continuation.resume(throwing: IndianaClientError.unauthenticated) }
62
+ }
63
+ resolve(nil)
64
+ } catch { fail(reject, error) }
65
+ }
66
+ private func action(_ owner: Instance, revision: UUID, _ payload: IndianaJSON) async throws {
67
+ guard owner.revision == revision else { throw IndianaClientError.identityChanged }
68
+ guard let id = instances.first(where: { $0.value === owner })?.key else { throw IndianaClientError.identityChanged }
69
+ let request = UUID().uuidString
70
+ try await withCheckedThrowingContinuation { continuation in
71
+ owner.actions[request] = continuation
72
+ do {
73
+ let object = try JSONSerialization.jsonObject(with: JSONEncoder().encode(payload))
74
+ sendEvent(withName: "IndianaAction", body: ["instanceId": id, "requestId": request, "revision": owner.identitySequence, "action": object])
75
+ } catch { owner.actions.removeValue(forKey: request)?.resume(throwing: error) }
76
+ Task { @MainActor in
77
+ try? await Task.sleep(for: .seconds(45))
78
+ owner.actions.removeValue(forKey: request)?.resume(throwing: IndianaClientError.temporaryUnavailable)
79
+ }
80
+ }
81
+ }
82
+ @objc(acknowledge:requestId:accepted:resolver:rejecter:)
83
+ func acknowledge(_ id: String, requestId: String, accepted: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
84
+ do {
85
+ let owner = try instance(id)
86
+ if let continuation = owner.actions.removeValue(forKey: requestId) {
87
+ if accepted { continuation.resume() } else { continuation.resume(throwing: IndianaClientError.unsupportedCapability) }
88
+ }
89
+ resolve(nil)
90
+ } catch { fail(reject, error) }
91
+ }
92
+
93
+ private func json(_ text: String) throws -> Data {
94
+ guard let data = text.data(using: .utf8), data.count <= 2_097_152 else { throw IndianaClientError.invalidResponse }
95
+ return data
96
+ }
97
+ private func instance(_ id: String) throws -> Instance {
98
+ guard let value = instances[id] else { throw IndianaClientError.invalidConfiguration }
99
+ return value
100
+ }
101
+ private func encode<T: Encodable>(_ value: T) throws -> String {
102
+ guard let text = String(data: try JSONEncoder().encode(value), encoding: .utf8) else { throw IndianaClientError.invalidResponse }
103
+ return text
104
+ }
105
+ private func fail(_ reject: RCTPromiseRejectBlock, _ error: Error) {
106
+ // Credentials and provider payloads never enter bridge error messages.
107
+ reject("INDIANA_NATIVE_ERROR", String(describing: type(of: error)), nil)
108
+ }
109
+
110
+ @objc(create:resolver:rejecter:)
111
+ func create(_ configuration: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
112
+ Task {
113
+ do {
114
+ let input = try JSONDecoder().decode(Configuration.self, from: json(configuration))
115
+ guard let url = URL(string: input.baseUrl) else { throw IndianaClientError.invalidConfiguration }
116
+ let id = UUID().uuidString
117
+ instances[id] = Instance(IndianaClient(configuration: try IndianaConfiguration(baseURL: url, scope: input.scope, timeout: input.timeout)))
118
+ if input.credentialEvents == true { await instances[id]!.client.setAccessTokenProvider { @MainActor [weak self] in
119
+ guard let self else { throw IndianaClientError.identityChanged }
120
+ return try await self.credential(id)
121
+ } }
122
+ resolve(id)
123
+ } catch { fail(reject, error) }
124
+ }
125
+ }
126
+
127
+ @objc(identify:session:resolver:rejecter:)
128
+ func identify(_ id: String, session: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
129
+ Task {
130
+ do {
131
+ let value = try instance(id)
132
+ let frame = try JSONDecoder().decode([String: IndianaJSON].self, from: json(session))
133
+ guard case .number(let number) = frame["revision"], number >= 0, number <= 9_007_199_254_740_991, number.rounded() == number, let credentials = frame["credentials"] else { throw IndianaClientError.invalidResponse }
134
+ let sequence = Int(number)
135
+ guard sequence >= value.identitySequence else { throw IndianaClientError.identityChanged }
136
+ value.identitySequence = sequence
137
+ let data = try JSONEncoder().encode(credentials)
138
+ value.revision = UUID(); value.close?(); value.close = nil; value.presentation = nil; cancelActions(value)
139
+ let token = try JSONDecoder().decode(String.self, from: data)
140
+ try await value.client.identify(customerToken: token)
141
+ resolve(nil)
142
+ } catch { fail(reject, error) }
143
+ }
144
+ }
145
+
146
+ @objc(reset:revision:resolver:rejecter:)
147
+ func reset(_ id: String, revision: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
148
+ Task {
149
+ do {
150
+ let value = try instance(id)
151
+ let number = revision.doubleValue
152
+ guard number.isFinite, number >= 0, number <= 9_007_199_254_740_991, number.rounded() == number else { throw IndianaClientError.invalidResponse }
153
+ guard revision.intValue >= value.identitySequence else { resolve(nil); return }
154
+ value.identitySequence = revision.intValue
155
+ value.revision = UUID(); value.close?(); value.close = nil; value.presentation = nil; cancelActions(value)
156
+ await value.client.reset()
157
+ resolve(nil)
158
+ } catch { fail(reject, error) }
159
+ }
160
+ }
161
+
162
+ @objc(destroy:resolver:rejecter:)
163
+ func destroy(_ id: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
164
+ Task {
165
+ guard let value = instances.removeValue(forKey: id) else { resolve(nil); return }
166
+ value.revision = UUID(); value.close?(); value.close = nil; cancelActions(value)
167
+ await value.client.reset()
168
+ resolve(nil)
169
+ }
170
+ }
171
+
172
+ @objc(invoke:method:input:resolver:rejecter:)
173
+ func invoke(_ id: String, method: String, input: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
174
+ Task {
175
+ do {
176
+ let owner = try instance(id), revision = owner.revision
177
+ let fields = try JSONDecoder().decode([String: IndianaJSON].self, from: json(input))
178
+ switch method {
179
+ case "restorePurchases":
180
+ let results = try await owner.client.restorePurchases()
181
+ guard revision == owner.revision else { throw IndianaClientError.identityChanged }
182
+ resolve(try encode(results.map { try JSONDecoder().decode(IndianaJSON.self, from: $0) }))
183
+ case "purchaseSubscription", "switchSubscription":
184
+ guard fields["accepted"] == .bool(true), let raw = fields["operationId"]?.string, let operationId = UUID(uuidString: raw) else { throw IndianaClientError.invalidResponse }
185
+ let result = method == "purchaseSubscription"
186
+ ? try await owner.client.purchaseSubscription(operationId: operationId, confirm: { _ in true })
187
+ : try await owner.client.switchSubscription(operationId: operationId, confirm: { _ in true })
188
+ guard revision == owner.revision else { throw IndianaClientError.identityChanged }
189
+ resolve(String(data: result, encoding: .utf8))
190
+ case "manageSubscriptions":
191
+ guard let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first(where: { $0.activationState == .foregroundActive }) else { throw IndianaClientError.unsupportedCapability }
192
+ try await IndianaStoreSubscriptions.manage(in: scene)
193
+ resolve("null")
194
+ case "showFunnel", "showPaywall", "showContent":
195
+ try await present(owner, revision: revision, method: method, fields: fields, resolve: resolve, reject: reject)
196
+ default: throw IndianaClientError.unsupportedCapability
197
+ }
198
+ } catch { fail(reject, error) }
199
+ }
200
+ }
201
+
202
+ private func presenter() throws -> UIViewController {
203
+ guard let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first(where: { $0.activationState == .foregroundActive }),
204
+ var controller = scene.windows.first(where: { $0.isKeyWindow })?.rootViewController else { throw IndianaClientError.unsupportedCapability }
205
+ while let next = controller.presentedViewController { controller = next }
206
+ return controller
207
+ }
208
+
209
+ private func present(_ owner: Instance, revision: UUID, method: String, fields: [String: IndianaJSON], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) async throws {
210
+ guard owner.presentation == nil else { throw IndianaClientError.unsupportedCapability }
211
+ let presenter = try presenter()
212
+ let reservation = UUID()
213
+ owner.presentation = reservation
214
+ var shown = false
215
+ defer { if !shown, owner.presentation == reservation { owner.presentation = nil } }
216
+ var settled = false
217
+ var controller: UIViewController!
218
+ var release: (() -> Void)?
219
+ let finish: @MainActor @Sendable (IndianaJSON) -> Void = { value in
220
+ guard !settled else { return }; settled = true
221
+ if owner.presentation == reservation { owner.close = nil; owner.presentation = nil }
222
+ release?()
223
+ controller?.dismiss(animated: true)
224
+ let result = owner.revision == revision ? value : .object(["status": .string("dismissed")])
225
+ do { resolve(try self.encode(result)) } catch { self.fail(reject, error) }
226
+ }
227
+ let dismissed: @MainActor @Sendable () -> Void = { finish(.object(["status": .string("dismissed")])) }
228
+ let failed: @MainActor @Sendable (Error) -> Void = { error in
229
+ guard !settled else { return }; settled = true
230
+ if owner.presentation == reservation { owner.close = nil; owner.presentation = nil }
231
+ release?()
232
+ controller?.dismiss(animated: true); self.fail(reject, error)
233
+ }
234
+ let purchase: @MainActor @Sendable (IndianaJSON) async throws -> Void = { operation in
235
+ guard owner.revision == revision else { throw IndianaClientError.identityChanged }
236
+ guard let raw = operation.object?["operationId"]?.string, let id = UUID(uuidString: raw) else { throw IndianaClientError.invalidResponse }
237
+ _ = try await owner.client.purchaseSubscription(operationId: id, confirm: { _ in true })
238
+ }
239
+ if method == "showFunnel" {
240
+ guard let token = fields["runToken"]?.string else { throw IndianaClientError.invalidResponse }
241
+ let session = try await owner.client.funnel(runToken: token)
242
+ let view = IndianaFunnelViewController(session: session)
243
+ view.onPurchase = purchase; view.onDismiss = dismissed; view.onError = failed
244
+ view.onEvent = { name in try await self.action(owner, revision: revision, .object(["type": .string("event"), "name": .string(name)])) }
245
+ view.onComplete = { run in
246
+ do { finish(.object(["status": .string("complete"), "result": try JSONDecoder().decode(IndianaJSON.self, from: JSONEncoder().encode(run))])) }
247
+ catch { failed(error) }
248
+ }
249
+ controller = view
250
+ release = { Task { await view.close() } }
251
+ } else if method == "showPaywall" {
252
+ guard let raw = fields["provider"]?.string, let provider = IndianaSubscriptionProvider(rawValue: raw), let currency = fields["currency"]?.string else { throw IndianaClientError.invalidResponse }
253
+ let session = try await owner.client.paywall(paywallId: fields["paywallId"]?.string.flatMap(UUID.init(uuidString:)), paywallVersionId: fields["paywallVersionId"]?.string.flatMap(UUID.init(uuidString:)), provider: provider, currency: currency, locale: fields["locale"]?.string ?? "en")
254
+ let view = IndianaPaywallViewController(session: session)
255
+ view.onPurchase = purchase; view.onDismiss = dismissed; view.onError = failed
256
+ view.onEvent = { name in try await self.action(owner, revision: revision, .object(["type": .string("event"), "name": .string(name)])) }
257
+ view.onComplete = { result in finish(.object(["status": .string("complete"), "result": result])) }
258
+ controller = view
259
+ release = { Task { await view.close() } }
260
+ } else {
261
+ guard let selector = fields["selector"], let kind = fields["kind"] else { throw IndianaClientError.invalidResponse }
262
+ let session = try await owner.client.content(["kind": kind, "selector": selector], locale: fields["locale"]?.string ?? "en", inputs: fields["inputs"]?.object ?? [:])
263
+ let view = IndianaContentViewController(session: session)
264
+ view.onDismiss = dismissed; view.onError = failed
265
+ view.onAction = { payload in try await self.action(owner, revision: revision, payload) }
266
+ controller = view
267
+ release = { Task { await session.close() } }
268
+ }
269
+ guard revision == owner.revision, owner.presentation == reservation else { release?(); throw IndianaClientError.identityChanged }
270
+ owner.close = dismissed
271
+ controller.isModalInPresentation = true
272
+ presenter.present(controller, animated: true)
273
+ shown = true
274
+ }
275
+ }