Skip to content

Commit fa8b5d3

Browse files
authored
Error controller mobile exposure (#783)
* Error controller expose to iOS/Android
1 parent 9c5d739 commit fa8b5d3

14 files changed

Lines changed: 1261 additions & 0 deletions

File tree

‎ios/core/Sources/Player/HeadlessPlayer.swift‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public protocol CoreHooks {
5656
/// Fired when the DataController changes
5757
var dataController: Hook<DataControllerType> { get }
5858

59+
/// Fired when the ErrorController changes
60+
var errorController: Hook<ErrorController> { get }
61+
5962
/// Fired when the state changes
6063
var state: Hook<BaseFlowState> { get }
6164

‎ios/core/Sources/Types/Core/CompletedState.swift‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ public class PlayerControllers {
2626
/// The ExpressionEvaluator for the current flow
2727
public let expression: ExpressionEvaluator
2828

29+
/// The ErrorController for the current flow
30+
public let error: ErrorController
31+
2932
public init?(from value: JSValue?) {
3033
guard let controllers = value else { return nil }
3134
rawValue = controllers
3235
data = DataController.createInstance(value: rawValue.objectForKeyedSubscript("data"))
3336
flow = FlowController.createInstance(value: rawValue.objectForKeyedSubscript("flow"))
3437
view = ViewController.createInstance(value: rawValue.objectForKeyedSubscript("view"))
3538
expression = ExpressionEvaluator.createInstance(value: rawValue.objectForKeyedSubscript("expression"))
39+
error = ErrorController.createInstance(value: rawValue.objectForKeyedSubscript("error"))
3640
}
3741
}
3842

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//
2+
// ErrorController.swift
3+
// PlayerUI
4+
//
5+
// Created by Player Team
6+
//
7+
8+
import Foundation
9+
import JavaScriptCore
10+
11+
/**
12+
Severity levels for errors
13+
*/
14+
public enum ErrorSeverity: String {
15+
/// Cannot continue, flow must end
16+
case fatal
17+
/// Standard error, may allow recovery
18+
case error
19+
/// Non-blocking, logged for telemetry
20+
case warning
21+
}
22+
23+
/**
24+
Known error types for Player
25+
*/
26+
public struct ErrorTypes {
27+
public static let expression = "expression"
28+
public static let binding = "binding"
29+
public static let view = "view"
30+
public static let asset = "asset"
31+
public static let navigation = "navigation"
32+
public static let validation = "validation"
33+
public static let data = "data"
34+
public static let schema = "schema"
35+
public static let network = "network"
36+
public static let plugin = "plugin"
37+
}
38+
39+
/**
40+
Represents a Player error with metadata
41+
*/
42+
public class PlayerErrorInfo: CreatedFromJSValue {
43+
/// Typealias for associated type
44+
public typealias T = PlayerErrorInfo
45+
46+
/// The JSValue that backs this wrapper
47+
private let value: JSValue
48+
49+
/// The error message
50+
public var message: String {
51+
value.objectForKeyedSubscript("error")?.objectForKeyedSubscript("message")?.toString() ?? ""
52+
}
53+
54+
/// The error name
55+
public var name: String {
56+
value.objectForKeyedSubscript("error")?.objectForKeyedSubscript("name")?.toString() ?? ""
57+
}
58+
59+
/// Error category
60+
public var errorType: String {
61+
value.objectForKeyedSubscript("errorType")?.toString() ?? ""
62+
}
63+
64+
/// Impact level
65+
public var severity: ErrorSeverity? {
66+
guard let severityString = value.objectForKeyedSubscript("severity")?.toString() else {
67+
return nil
68+
}
69+
return ErrorSeverity(rawValue: severityString)
70+
}
71+
72+
/// Additional metadata
73+
public var metadata: [String: Any]? {
74+
guard let metadataValue = value.objectForKeyedSubscript("metadata"),
75+
!metadataValue.isUndefined,
76+
!metadataValue.isNull else {
77+
return nil
78+
}
79+
return metadataValue.toObject() as? [String: Any]
80+
}
81+
82+
/**
83+
Creates an instance from a JSValue, used for generic construction
84+
- parameters:
85+
- value: The JSValue to construct from
86+
*/
87+
public static func createInstance(value: JSValue) -> PlayerErrorInfo {
88+
return PlayerErrorInfo(value)
89+
}
90+
91+
/**
92+
Construct a PlayerErrorInfo from a JSValue
93+
- parameters:
94+
- value: The JSValue that is the error object
95+
*/
96+
public init(_ value: JSValue) {
97+
self.value = value
98+
}
99+
}
100+
101+
/**
102+
A wrapper around the JS ErrorController in the core player
103+
*/
104+
public class ErrorController: CreatedFromJSValue {
105+
/// Typealias for associated type
106+
public typealias T = ErrorController
107+
108+
/**
109+
Creates an instance from a JSValue, used for generic construction
110+
- parameters:
111+
- value: The JSValue to construct from
112+
*/
113+
public static func createInstance(value: JSValue) -> ErrorController { ErrorController(value) }
114+
115+
/// The JSValue that backs this wrapper
116+
private let value: JSValue
117+
118+
/// The hooks that can be tapped into
119+
public let hooks: ErrorControllerHooks
120+
121+
/**
122+
Construct an ErrorController from a JSValue
123+
- parameters:
124+
- value: The JSValue that is the ErrorController
125+
*/
126+
public init(_ value: JSValue) {
127+
self.value = value
128+
hooks = ErrorControllerHooks(
129+
onError: BailHook<PlayerErrorInfo>(baseValue: value, name: "onError")
130+
)
131+
}
132+
133+
/**
134+
Capture an error with metadata
135+
- parameters:
136+
- error: The native Error object
137+
- errorType: Error category (use ErrorTypes constants)
138+
- severity: Impact level
139+
- metadata: Additional metadata dictionary
140+
- returns: The captured error as a JSValue
141+
*/
142+
@discardableResult
143+
public func captureError(
144+
error: Error,
145+
errorType: String,
146+
severity: ErrorSeverity? = nil,
147+
metadata: [String: Any]? = nil
148+
) -> JSValue? {
149+
var args: [Any] = [
150+
[
151+
"message": error.localizedDescription,
152+
"name": String(describing: type(of: error))
153+
] as [String: Any],
154+
errorType
155+
]
156+
157+
if let severity = severity {
158+
args.append(severity.rawValue)
159+
} else {
160+
args.append(JSValue(undefinedIn: value.context) as Any)
161+
}
162+
163+
if let metadata = metadata {
164+
args.append(metadata)
165+
}
166+
167+
return value.invokeMethod("captureError", withArguments: args)
168+
}
169+
170+
/**
171+
Get the most recent error
172+
- returns: The current error as a JSValue if one exists
173+
*/
174+
public func getCurrentError() -> JSValue? {
175+
return value.invokeMethod("getCurrentError", withArguments: [])
176+
}
177+
178+
/**
179+
Get the complete error history
180+
- returns: JSValue representing the array of errors
181+
*/
182+
public func getErrors() -> JSValue? {
183+
return value.invokeMethod("getErrors", withArguments: [])
184+
}
185+
186+
/**
187+
Clear all errors (history + current + data model)
188+
*/
189+
public func clearErrors() {
190+
value.invokeMethod("clearErrors", withArguments: [])
191+
}
192+
193+
/**
194+
Clear only current error and remove from data model, preserve history
195+
*/
196+
public func clearCurrentError() {
197+
value.invokeMethod("clearCurrentError", withArguments: [])
198+
}
199+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// ErrorControllerHooks.swift
3+
// PlayerUI
4+
//
5+
// Created by Player Team
6+
//
7+
8+
import Foundation
9+
import JavaScriptCore
10+
11+
/**
12+
Hooks that can be tapped into for the ErrorController
13+
This lets users tap into error events in the JS environment
14+
*/
15+
public struct ErrorControllerHooks {
16+
/**
17+
Fired when any error is captured
18+
- The callback receives a PlayerErrorInfo object
19+
- Return true from the callback to bail and prevent error state navigation
20+
- Return nil/false to allow automatic navigation to continue
21+
*/
22+
public var onError: BailHook<PlayerErrorInfo>
23+
}
24+

‎ios/core/Sources/Types/Hooks/Hook.swift‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,39 @@ public class Hook<T>: BaseJSHook where T: CreatedFromJSValue {
8181
}
8282
}
8383

84+
/**
85+
This class represents a SyncBailHook in the JS runtime that can be tapped into
86+
to receive JS events and optionally return a value to bail/stop further execution
87+
*/
88+
public class BailHook<T>: BaseJSHook where T: CreatedFromJSValue {
89+
/**
90+
Attach a closure to the hook, so when the hook is fired in the JS runtime
91+
we receive the event in the native runtime and can return a value to bail
92+
93+
- parameters:
94+
- hook: A function to run when the JS hook is fired, return Bool? to bail (return nil to continue)
95+
*/
96+
public func tap(_ hook: @escaping (T) -> Bool?) {
97+
let tapMethod: @convention(block) (JSValue?) -> JSValue? = { [weak self] value in
98+
guard
99+
let self = self,
100+
let val = value,
101+
let hookValue = T.createInstance(value: val) as? T
102+
else { return nil }
103+
104+
let result = hook(hookValue)
105+
if let boolResult = result {
106+
// Return JS true/false to bail
107+
return JSValue(bool: boolResult, in: self.context)
108+
}
109+
// Return JS undefined to continue
110+
return JSValue(undefinedIn: self.context)
111+
}
112+
113+
self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any])
114+
}
115+
}
116+
84117
/**
85118
This class represents an object in the JS runtime that can be tapped into
86119
to receive JS events that has 2 parameters

0 commit comments

Comments
 (0)