@tinkerlandai/indiana-react-native 0.9.2 → 0.10.1
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 +62 -5
- 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 +96889 -6007
- package/dist/index.d.ts +96889 -6007
- 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 +197 -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/MerchantCommerceSemantics.kt +38 -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 +236 -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/MerchantCommerceSemantics.swift +51 -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,125 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import Foundation
|
|
3
|
+
import UIKit
|
|
4
|
+
@preconcurrency import WebKit
|
|
5
|
+
@preconcurrency import SafariServices
|
|
6
|
+
import SwiftUI
|
|
7
|
+
|
|
8
|
+
/// A native container for the single published Funnel renderer. Embed as a child or present over the app.
|
|
9
|
+
@MainActor public final class IndianaFunnelViewController: UIViewController, @preconcurrency SFSafariViewControllerDelegate {
|
|
10
|
+
public let session: IndianaFunnelSession
|
|
11
|
+
public var onPurchase: (@MainActor (IndianaJSON) async throws -> Void)?
|
|
12
|
+
public var onEvent: (@MainActor (String) async throws -> Void)?
|
|
13
|
+
/// Opens an external page or Paywall link (`openIn` is `same_window` or `new_window`). Return false when it could
|
|
14
|
+
/// not be opened; an external page then takes its failed route. Default: in-app browser for `same_window`,
|
|
15
|
+
/// the system browser for `new_window`.
|
|
16
|
+
public var onOpenURL: (@MainActor (URL, String) async throws -> Bool)?
|
|
17
|
+
public var onComplete: (@MainActor (IndianaFunnelRun) -> Void)?
|
|
18
|
+
public var onDismiss: (@MainActor () -> Void)?
|
|
19
|
+
public var onError: (@MainActor (Error) -> Void)?
|
|
20
|
+
private var host: IndianaBundledWebView!
|
|
21
|
+
private var configurationRelay: IndianaFunnelConfigurationRelay?
|
|
22
|
+
private var stopped = false; private var completed = false; private var dismissed = false
|
|
23
|
+
private var observer: IndianaForegroundObserver?
|
|
24
|
+
public init(session: IndianaFunnelSession) { self.session = session; super.init(nibName: nil, bundle: nil) }
|
|
25
|
+
required init?(coder: NSCoder) { fatalError("Use init(session:)") }
|
|
26
|
+
public override func loadView() {
|
|
27
|
+
host = IndianaBundledWebView(label:"Funnel", dispatch: { [weak self] method,payload in
|
|
28
|
+
guard let self, !self.stopped else { throw IndianaFunnelError.identityChanged }
|
|
29
|
+
return try await self.dispatch(method,payload:payload)
|
|
30
|
+
},onError: { [weak self] error in self?.onError?(error) })
|
|
31
|
+
view = host.webView
|
|
32
|
+
Task {
|
|
33
|
+
await session.onInvalidation { [weak self] in Task { @MainActor in self?.invalidate() } }
|
|
34
|
+
guard !stopped else { return }; host.start()
|
|
35
|
+
}
|
|
36
|
+
observer = IndianaForegroundObserver { [weak self] in
|
|
37
|
+
Task { @MainActor in self?.sendEvent("refresh") }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
public func close() async {
|
|
41
|
+
guard !stopped else { return }
|
|
42
|
+
do { _ = try await session.close() } catch { onError?(error) }
|
|
43
|
+
invalidate()
|
|
44
|
+
}
|
|
45
|
+
private func invalidate() {
|
|
46
|
+
guard !stopped else { return }
|
|
47
|
+
host?.destroy(); stopped = true; configurationRelay = nil
|
|
48
|
+
presentedViewController?.dismiss(animated: false)
|
|
49
|
+
if !dismissed { dismissed = true; onDismiss?() }
|
|
50
|
+
}
|
|
51
|
+
private func dispatch(_ method: String, payload: IndianaJSON) async throws -> IndianaJSON {
|
|
52
|
+
switch method {
|
|
53
|
+
case "ready":
|
|
54
|
+
let config = await session.hostConfiguration()
|
|
55
|
+
// UUID's synthesized Codable form is uppercase; the wire scope is canonical lowercase.
|
|
56
|
+
let scope: IndianaJSON = .object([
|
|
57
|
+
"projectId": .string(config.scope.projectId.uuidString.lowercased()),
|
|
58
|
+
"environment": .string(config.scope.environment.rawValue),
|
|
59
|
+
])
|
|
60
|
+
send(.object(["event":.string("init"),"scope":scope,"apiOrigin":.string(config.apiOrigin)])); return .null
|
|
61
|
+
case "pendingCommand": return await session.hostPendingCommand()
|
|
62
|
+
case "configuration":
|
|
63
|
+
if configurationRelay == nil { configurationRelay = IndianaFunnelConfigurationRelay(configuration: await session.hostConfiguration()) }
|
|
64
|
+
guard let relay = configurationRelay, !stopped else { throw IndianaFunnelError.identityChanged }
|
|
65
|
+
return try await relay.read(payload)
|
|
66
|
+
case "paywallContent":
|
|
67
|
+
return try await session.hostPaywallContent(payload)
|
|
68
|
+
case "command":
|
|
69
|
+
guard let command = payload.object else { throw IndianaFunnelError.invalidCommand }
|
|
70
|
+
return try JSONDecoder().decode(IndianaJSON.self, from: await session.hostCommand(command))
|
|
71
|
+
case "purchase":
|
|
72
|
+
guard let operation = payload.object?["operation"], let onPurchase else { throw IndianaClientError.unsupportedCapability }
|
|
73
|
+
let trusted = try await session.trustedPurchase(operation)
|
|
74
|
+
try await onPurchase(trusted);return .null
|
|
75
|
+
case "checkout":
|
|
76
|
+
guard let raw = payload.object?["url"]?.string else { throw IndianaFunnelError.invalidCommand }
|
|
77
|
+
let url = try await session.trustedCheckoutURL(raw)
|
|
78
|
+
let browser = SFSafariViewController(url: url);browser.delegate = self;present(browser, animated: true);return .null
|
|
79
|
+
case "open":
|
|
80
|
+
let url = try await session.trustedOpen(payload), openIn = payload.object?["openIn"]?.string ?? "same_window"
|
|
81
|
+
if let onOpenURL { return .bool(try await onOpenURL(url, openIn)) }
|
|
82
|
+
if openIn == "new_window" { return .bool(await UIApplication.shared.open(url)) }
|
|
83
|
+
let browser = SFSafariViewController(url: url);browser.delegate = self;present(browser, animated: true);return .bool(true)
|
|
84
|
+
case "event":
|
|
85
|
+
guard let name = payload.object?["event"]?.string, let onEvent else { throw IndianaClientError.unsupportedCapability }
|
|
86
|
+
try await onEvent(try await session.trustedEvent(name)); return .null
|
|
87
|
+
case "complete":
|
|
88
|
+
guard let run = await session.current, run.status == "completed", run.instruction == nil || run.instruction == .null else { throw IndianaFunnelError.invalidCommand }
|
|
89
|
+
if !completed { completed = true; onComplete?(run) }; return .null
|
|
90
|
+
case "dismiss":
|
|
91
|
+
if !dismissed { dismissed = true; onDismiss?() }; return .null
|
|
92
|
+
default: throw IndianaFunnelError.invalidCommand
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
private func send(_ value: IndianaJSON) { host?.send(value) }
|
|
96
|
+
private func sendEvent(_ event: String) { send(.object(["event":.string(event)])) }
|
|
97
|
+
public func safariViewControllerDidFinish(_ controller: SFSafariViewController) { controller.dismiss(animated: true); sendEvent("refresh") }
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
final class IndianaForegroundObserver {
|
|
102
|
+
private let token: NSObjectProtocol
|
|
103
|
+
init(_ action: @escaping @Sendable () -> Void) {
|
|
104
|
+
token = NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { _ in action() }
|
|
105
|
+
}
|
|
106
|
+
deinit { NotificationCenter.default.removeObserver(token) }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Supports embedding in an existing product screen; node presentation stays authored by the Funnel.
|
|
110
|
+
@MainActor public struct IndianaFunnelView: UIViewControllerRepresentable {
|
|
111
|
+
public let session: IndianaFunnelSession
|
|
112
|
+
public var onPurchase: (@MainActor (IndianaJSON) async throws -> Void)?
|
|
113
|
+
public var onEvent: (@MainActor (String) async throws -> Void)?
|
|
114
|
+
public var onComplete: (@MainActor (IndianaFunnelRun) -> Void)?
|
|
115
|
+
public var onDismiss: (@MainActor () -> Void)?
|
|
116
|
+
public var onError: (@MainActor (Error) -> Void)?
|
|
117
|
+
public var onOpenURL: (@MainActor (URL, String) async throws -> Bool)?
|
|
118
|
+
public init(session: IndianaFunnelSession, onPurchase: (@MainActor (IndianaJSON) async throws -> Void)? = nil, onEvent: (@MainActor (String) async throws -> Void)? = nil, onComplete: (@MainActor (IndianaFunnelRun) -> Void)? = nil, onDismiss: (@MainActor () -> Void)? = nil, onError: (@MainActor (Error) -> Void)? = nil, onOpenURL: (@MainActor (URL, String) async throws -> Bool)? = nil) {
|
|
119
|
+
self.session = session;self.onPurchase = onPurchase;self.onEvent = onEvent;self.onComplete = onComplete;self.onDismiss = onDismiss;self.onError = onError;self.onOpenURL = onOpenURL
|
|
120
|
+
}
|
|
121
|
+
public func makeUIViewController(context: Context) -> IndianaFunnelViewController { let controller = IndianaFunnelViewController(session: session); updateUIViewController(controller, context: context); return controller }
|
|
122
|
+
public func updateUIViewController(_ controller: IndianaFunnelViewController, context: Context) { controller.onPurchase = onPurchase; controller.onEvent = onEvent; controller.onComplete = onComplete; controller.onDismiss = onDismiss; controller.onError = onError; controller.onOpenURL = onOpenURL }
|
|
123
|
+
public static func dismantleUIViewController(_ controller: IndianaFunnelViewController, coordinator: ()) { Task { await controller.close() } }
|
|
124
|
+
}
|
|
125
|
+
#endif
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated by scripts/sdk/generate-native-protocol.mjs. Do not edit.
|
|
2
|
+
import Foundation
|
|
3
|
+
public enum IndianaEnvironment: String, Codable, Sendable { case sandbox, live }
|
|
4
|
+
enum IndianaProtocol {
|
|
5
|
+
static let wireProtocol = "0.10"
|
|
6
|
+
static let uuidPattern = "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
7
|
+
static let utcPattern = "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
|
|
8
|
+
static let scopeKeys: Set<String> = ["projectId", "environment"]
|
|
9
|
+
static let runKeys: Set<String> = ["id", "source", "revision", "visit", "nodeId", "nodeType", "status", "stopped", "canBack", "authenticated", "locale", "answers", "parameters", "variables", "output", "screen", "paywall", "instruction", "pendingRequest", "runExpiresAt", "credentialExpiresAt"]
|
|
10
|
+
static let runRequiredKeys: Set<String> = ["id", "source", "revision", "visit", "nodeId", "nodeType", "status", "stopped", "canBack", "authenticated", "locale", "answers", "parameters", "variables", "output", "screen", "paywall", "instruction", "pendingRequest", "runExpiresAt", "credentialExpiresAt"]
|
|
11
|
+
static let nodeTypes: Set<String> = ["entry", "screen", "condition", "split", "feature", "action", "external_page", "wait", "end", "offering", "coupon", "coupons", "output"]
|
|
12
|
+
static let runStatuses: Set<String> = ["running", "waiting", "completed", "closed", "ineligible", "failed"]
|
|
13
|
+
static let nonCanonicalLocaleLanguages: Set<String> = ["aam", "aar", "abk", "adp", "afr", "agp", "ais", "ajp", "ajt", "aju", "aka", "alb", "als", "amh", "ara", "arb", "arg", "arm", "asd", "asm", "aue", "ava", "ave", "aym", "ayr", "ayx", "aze", "azj", "bak", "bam", "baq", "baz", "bcc", "bcl", "bel", "ben", "bgm", "bhk", "bic", "bih", "bis", "bjd", "bjq", "bkb", "blg", "bod", "bos", "bre", "btb", "bul", "bur", "bxk", "bxr", "cat", "ccq", "ces", "cha", "che", "chi", "chu", "chv", "cjr", "cka", "cld", "cls", "cmk", "cmn", "cnr", "cor", "cos", "coy", "cqu", "cre", "cwd", "cym", "cze", "daf", "dan", "dap", "deu", "dgo", "dhd", "dik", "diq", "dit", "div", "djl", "dkl", "drh", "drr", "drw", "dud", "duj", "dut", "dwl", "dzo", "ekk", "ell", "elp", "emk", "eng", "epo", "esk", "est", "eus", "ewe", "fao", "fas", "fat", "fij", "fin", "fra", "fre", "fry", "fuc", "ful", "gav", "gaz", "gbc", "gbo", "geo", "ger", "gfx", "ggn", "ggo", "ggr", "gio", "gla", "gle", "glg", "gli", "glv", "gno", "gom", "gre", "grn", "gti", "gug", "guj", "guv", "gya", "hat", "hau", "hbs", "hdn", "hea", "heb", "her", "him", "hin", "hmo", "hrr", "hrv", "hun", "hye", "ibi", "ibo", "ice", "ido", "iii", "ike", "iku", "ile", "ill", "ilw", "in", "ina", "ind", "ipk", "isl", "ita", "iw", "izi", "jar", "jav", "jaw", "jeg", "ji", "jpn", "jw", "kal", "kan", "kas", "kat", "kau", "kaz", "kdv", "kgc", "kgd", "kgh", "kgm", "khk", "khm", "kik", "kin", "kir", "kmr", "knc", "kng", "koj", "kom", "kon", "kor", "kpp", "kpv", "krm", "ktr", "kua", "kur", "kvs", "kwq", "kxe", "kxl", "kzh", "kzj", "kzt", "lak", "lao", "lat", "lav", "lbk", "leg", "lii", "lim", "lin", "lit", "llo", "lmm", "ltz", "lub", "lug", "lvs", "mac", "mah", "mal", "mao", "mar", "may", "meg", "mgx", "mhr", "mkd", "mlg", "mlt", "mnk", "mnt", "mo", "mof", "mol", "mon", "mri", "msa", "mst", "mup", "mwd", "mwj", "mya", "myd", "myt", "nad", "nau", "nav", "nbf", "nbl", "nbx", "ncp", "nde", "ndo", "nep", "nld", "nln", "nlr", "nno", "nns", "nnx", "nob", "nom", "noo", "nor", "npi", "nts", "nxu", "nya", "oci", "ojg", "oji", "ori", "orm", "ory", "oss", "oun", "pan", "pat", "pbu", "pcr", "per", "pes", "pli", "plt", "pmc", "pmk", "pmu", "pnb", "pol", "por", "ppa", "ppr", "prp", "prs", "pry", "pus", "puz", "que", "quz", "rmr", "rmy", "roh", "ron", "rum", "run", "rus", "sag", "san", "sap", "sca", "scc", "scr", "sgl", "sh", "sin", "skk", "slk", "slo", "slv", "smd", "sme", "smo", "sna", "snb", "snd", "som", "sot", "spa", "spy", "sqi", "src", "srd", "srp", "ssw", "sul", "sum", "sun", "swa", "swc", "swe", "swh", "szd", "tah", "tam", "tat", "tdu", "tel", "tgg", "tgk", "tgl", "tha", "thc", "thw", "thx", "tib", "tid", "tie", "tir", "tkk", "tl", "tlw", "tmk", "tmp", "tne", "tnf", "ton", "tpw", "tsf", "tsn", "tso", "ttq", "tuk", "tur", "twi", "uig", "ukr", "umu", "unp", "uok", "urd", "uzb", "uzn", "ven", "vie", "vol", "wel", "wgw", "wit", "wiw", "wln", "wol", "xba", "xho", "xia", "xkh", "xpe", "xrq", "xsj", "xsl", "xss", "ybd", "ydd", "yen", "yid", "yiy", "yma", "ymt", "yor", "yos", "yuu", "zai", "zha", "zho", "zir", "zkb", "zsm", "zul", "zyb"]
|
|
14
|
+
static let nonCanonicalLocaleScripts: Set<String> = ["Qaai"]
|
|
15
|
+
static let nonCanonicalLocaleRegions: Set<String> = ["AN", "BU", "CS", "CT", "DD", "DY", "FQ", "FX", "HV", "JT", "MI", "NH", "NQ", "NT", "PC", "PU", "PZ", "QU", "RH", "SU", "TP", "UK", "VD", "WK", "YD", "YU", "ZR", "004", "008", "010", "012", "016", "020", "024", "028", "031", "032", "036", "040", "044", "048", "050", "051", "052", "056", "060", "062", "064", "068", "070", "072", "074", "076", "084", "086", "090", "092", "096", "100", "104", "108", "112", "116", "120", "124", "132", "136", "140", "144", "148", "152", "156", "158", "162", "166", "170", "172", "174", "175", "178", "180", "184", "188", "191", "192", "196", "200", "203", "204", "208", "212", "214", "218", "222", "226", "230", "231", "232", "233", "234", "238", "239", "242", "246", "248", "249", "250", "254", "258", "260", "262", "266", "268", "270", "275", "276", "278", "280", "288", "292", "296", "300", "304", "308", "312", "316", "320", "324", "328", "332", "334", "336", "340", "344", "348", "352", "356", "360", "364", "368", "372", "376", "380", "384", "388", "392", "398", "400", "404", "408", "410", "414", "417", "418", "422", "426", "428", "430", "434", "438", "440", "442", "446", "450", "454", "458", "462", "466", "470", "474", "478", "480", "484", "492", "496", "498", "499", "500", "504", "508", "512", "516", "520", "524", "528", "530", "531", "532", "533", "534", "535", "536", "540", "548", "554", "558", "562", "566", "570", "574", "578", "580", "581", "582", "583", "584", "585", "586", "591", "598", "600", "604", "608", "612", "616", "620", "624", "626", "630", "634", "638", "642", "643", "646", "652", "654", "659", "660", "662", "663", "666", "670", "674", "678", "682", "686", "688", "690", "694", "702", "703", "704", "705", "706", "710", "716", "720", "724", "728", "729", "732", "736", "740", "744", "748", "752", "756", "760", "762", "764", "768", "772", "776", "780", "784", "788", "792", "795", "796", "798", "800", "804", "807", "810", "818", "826", "830", "831", "832", "833", "834", "840", "850", "854", "858", "860", "862", "876", "882", "886", "887", "890", "891", "894", "958", "959", "960", "962", "963", "964", "965", "966", "967", "968", "969", "970", "971", "972", "973", "974", "975", "976", "977", "978", "979", "980", "981", "982", "983", "984", "985", "986", "987", "988", "989", "990", "991", "992", "993", "994", "995", "996", "997", "998", "999"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public struct IndianaConfiguration: Sendable {
|
|
4
|
+
public let baseURL: URL
|
|
5
|
+
internal var apiOrigin: String {
|
|
6
|
+
var parts = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
|
|
7
|
+
parts.path = ""; parts.query = nil; parts.fragment = nil
|
|
8
|
+
parts.host = parts.host?.lowercased(); if parts.port == 443 { parts.port = nil }
|
|
9
|
+
return parts.string!
|
|
10
|
+
}
|
|
11
|
+
public let scope: IndianaScope
|
|
12
|
+
public let timeout: TimeInterval
|
|
13
|
+
/// Override baseURL explicitly for test services; Sandbox is a business scope, not a deployment selector.
|
|
14
|
+
public init(baseURL: URL = URL(string: "https://api.tinker.cash")!, scope: IndianaScope, timeout: TimeInterval = 15) throws {
|
|
15
|
+
guard baseURL.scheme == "https", baseURL.host != nil, baseURL.user == nil, baseURL.password == nil,
|
|
16
|
+
baseURL.query == nil, baseURL.fragment == nil, timeout > 0, timeout <= 120 else { throw IndianaClientError.invalidConfiguration }
|
|
17
|
+
self.baseURL = baseURL; self.scope = scope; self.timeout = timeout
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
protocol IndianaHTTP: Sendable {
|
|
21
|
+
func send(path: String, method: String, authorization: String, body: Data?) async throws -> Data
|
|
22
|
+
}
|
|
23
|
+
final class IndianaURLTransport: IndianaHTTP, Sendable {
|
|
24
|
+
static func requireCurrentProtocol(_ value: String?) throws {
|
|
25
|
+
guard value == IndianaProtocol.wireProtocol else { throw IndianaClientError.invalidResponse }
|
|
26
|
+
}
|
|
27
|
+
private let configuration: IndianaConfiguration
|
|
28
|
+
private let requestOrigin: URL
|
|
29
|
+
private let session: URLSession
|
|
30
|
+
// Internal test seam: the public configuration remains HTTPS-only. A wire fixture
|
|
31
|
+
// may use only a loopback HTTP origin; no public SDK caller can select it.
|
|
32
|
+
init(configuration: IndianaConfiguration, sessionConfiguration: URLSessionConfiguration = .ephemeral, loopbackFixtureOrigin: URL? = nil) {
|
|
33
|
+
self.configuration = configuration
|
|
34
|
+
if let origin = loopbackFixtureOrigin {
|
|
35
|
+
precondition(origin.scheme == "http" && origin.host == "127.0.0.1" && (origin.port ?? 0) > 1024 && origin.user == nil && origin.password == nil && origin.query == nil && origin.fragment == nil && origin.path.isEmpty)
|
|
36
|
+
}
|
|
37
|
+
self.requestOrigin = loopbackFixtureOrigin ?? configuration.baseURL
|
|
38
|
+
let config = sessionConfiguration.copy() as! URLSessionConfiguration
|
|
39
|
+
config.urlCache = nil; config.httpCookieStorage = nil; config.httpShouldSetCookies = false; config.urlCredentialStorage = nil; config.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
40
|
+
let ownedHeaders = Set(["authorization", "cookie", "x-indiana-project", "x-indiana-environment", "x-indiana-protocol", "x-indiana-sdk-version"])
|
|
41
|
+
config.httpAdditionalHeaders = config.httpAdditionalHeaders?.filter { !ownedHeaders.contains(String(describing: $0.key).lowercased()) }
|
|
42
|
+
config.timeoutIntervalForRequest = configuration.timeout; config.timeoutIntervalForResource = configuration.timeout
|
|
43
|
+
self.session = URLSession(configuration: config, delegate: NoRedirect(), delegateQueue: nil)
|
|
44
|
+
}
|
|
45
|
+
func send(path: String, method: String, authorization: String, body: Data?) async throws -> Data {
|
|
46
|
+
guard !path.contains(".."), !path.contains("?"), !path.contains("#"), !path.hasPrefix("/") else { throw IndianaClientError.invalidConfiguration }
|
|
47
|
+
var request = URLRequest(url: requestOrigin.appendingPathComponent("api/v1").appendingPathComponent(path))
|
|
48
|
+
request.httpMethod = method; request.httpBody = body
|
|
49
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
50
|
+
if !authorization.isEmpty { request.setValue("Bearer \(authorization)", forHTTPHeaderField: "Authorization") }
|
|
51
|
+
request.setValue(configuration.scope.projectId.uuidString.lowercased(), forHTTPHeaderField: "X-Indiana-Project")
|
|
52
|
+
request.setValue(configuration.scope.environment.rawValue, forHTTPHeaderField: "X-Indiana-Environment")
|
|
53
|
+
request.setValue(IndianaProtocol.wireProtocol, forHTTPHeaderField: "X-Indiana-Protocol")
|
|
54
|
+
request.setValue(IndianaSDK.version, forHTTPHeaderField: "X-Indiana-SDK-Version")
|
|
55
|
+
do {
|
|
56
|
+
let (bytes, response) = try await session.bytes(for: request)
|
|
57
|
+
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
|
|
58
|
+
throw IndianaClientError.requestFailed((response as? HTTPURLResponse)?.statusCode ?? 0)
|
|
59
|
+
}
|
|
60
|
+
try Self.requireCurrentProtocol(http.value(forHTTPHeaderField: "X-Indiana-Protocol"))
|
|
61
|
+
if response.expectedContentLength > 2_097_152 { throw IndianaClientError.invalidResponse }
|
|
62
|
+
var data = Data()
|
|
63
|
+
for try await byte in bytes {
|
|
64
|
+
guard data.count < 2_097_152 else { throw IndianaClientError.invalidResponse }
|
|
65
|
+
data.append(byte)
|
|
66
|
+
}
|
|
67
|
+
return data
|
|
68
|
+
} catch let error as IndianaClientError { throw error }
|
|
69
|
+
catch { throw IndianaClientError.temporaryUnavailable }
|
|
70
|
+
}
|
|
71
|
+
private final class NoRedirect: NSObject, URLSessionTaskDelegate, Sendable {
|
|
72
|
+
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) }
|
|
73
|
+
}
|
|
74
|
+
}
|