diff --git a/ios/core/Sources/Player/HeadlessPlayer.swift b/ios/core/Sources/Player/HeadlessPlayer.swift index 59a337860..92f3ec8f0 100644 --- a/ios/core/Sources/Player/HeadlessPlayer.swift +++ b/ios/core/Sources/Player/HeadlessPlayer.swift @@ -137,6 +137,8 @@ public protocol HeadlessPlayer { var hooks: HooksType? { get } /// A logger reference for use in plugins to log through the shared player logger var logger: TapableLogger { get } + /// A reference to the Key/Value store for constants and context for player + var constantsController: ConstantsController? { get } /** Sets up the core javascript player in the given context @@ -166,6 +168,15 @@ public extension HeadlessPlayer { var state: BaseFlowState? { return jsPlayerReference?.getState() } + + var constantsController: ConstantsController? { + guard + let constantControllerJSValue = jsPlayerReference?.objectForKeyedSubscript("constantsController") + else { + return nil + } + return ConstantsController(constantsController: constantControllerJSValue) + } /** Sets up the core javascript player in the given context diff --git a/ios/core/Sources/Types/Core/ConstantsController.swift b/ios/core/Sources/Types/Core/ConstantsController.swift new file mode 100644 index 000000000..ab45fd08e --- /dev/null +++ b/ios/core/Sources/Types/Core/ConstantsController.swift @@ -0,0 +1,31 @@ +import JavaScriptCore + +public class ConstantsController { + var constantsController: JSValue? + + public func getConstants(key: Any, namespace: String, fallback: Any? = nil) -> T? { + if let fallbackValue = fallback { + let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace, fallbackValue]) + return value?.toString() as? T + } else { + let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace]) + return value?.toString() as? T + } + } + + public func addConstants(data: Any, namespace: String) -> Void { + self.constantsController?.invokeMethod("addConstants", withArguments: [data, namespace]) + } + + public func setTemporaryValues(data: Any, namespace: String) -> Void { + self.constantsController?.invokeMethod("setTemporaryValues", withArguments: [data, namespace]) + } + + public func clearTemporaryValues() -> Void { + self.constantsController?.invokeMethod("clearTemporaryValues", withArguments: []) + } + + public init(constantsController: JSValue) { + self.constantsController = constantsController + } +} diff --git a/ios/core/Tests/HeadlessPlayerTests.swift b/ios/core/Tests/HeadlessPlayerTests.swift index 9299520e6..39e935f39 100644 --- a/ios/core/Tests/HeadlessPlayerTests.swift +++ b/ios/core/Tests/HeadlessPlayerTests.swift @@ -272,6 +272,101 @@ class HeadlessPlayerTests: XCTestCase { player.start(flow: FlowData.COUNTER) { _ in} wait(for: [updateExp], timeout: 1) } + + func testConstantsController() { + let player = HeadlessPlayerImpl(plugins: []) + + guard let constantsController = player.constantsController else { return } + + // Basic get/set tests + let data: Any = [ + "firstname": "john", + "lastname": "doe", + "favorite": [ + "color": "red" + ] + ] + + constantsController.addConstants(data: data, namespace: "constants") + + let firstname: String? = constantsController.getConstants(key: "firstname", namespace: "constants") + XCTAssertEqual(firstname, "john") + + let middleName: String? = constantsController.getConstants(key:"middlename", namespace: "constants") + XCTAssertEqual(middleName, "undefined") + + let middleNameSafe: String? = constantsController.getConstants(key:"middlename", namespace: "constants", fallback: "A") + XCTAssertEqual(middleNameSafe, "A") + + let favoriteColor: String? = constantsController.getConstants(key:"favorite.color", namespace: "constants") + XCTAssertEqual(favoriteColor, "red") + + let nonExistantNamespace: String? = constantsController.getConstants(key:"test", namespace: "foo") + XCTAssertEqual(nonExistantNamespace, "undefined") + + let nonExistantNamespaceWithFallback: String? = constantsController.getConstants(key:"test", namespace: "foo", fallback: "B") + XCTAssertEqual(nonExistantNamespaceWithFallback, "B") + + // Test and make sure keys override properly + let newData: Any = [ + "favorite": [ + "color": "blue", + ], + ]; + + constantsController.addConstants(data: newData, namespace: "constants"); + + let newFavoriteColor: String? = constantsController.getConstants(key: "favorite.color", namespace:"constants") + XCTAssertEqual(newFavoriteColor, "blue") + } + + func testConstantsControllerTempValues() { + let player = HeadlessPlayerImpl(plugins: []) + + guard let constantsController = player.constantsController else { return } + + // Add initial constants + let data: Any = [ + "firstname": "john", + "lastname": "doe", + "favorite": [ + "color": "red" + ] + ] + constantsController.addConstants(data: data, namespace: "constants") + + // Override with temporary values + let tempData: Any = [ + "firstname": "jane", + "favorite": [ + "color": "blue" + ] + ] + constantsController.setTemporaryValues(data:tempData, namespace: "constants") + + // Test temporary override + let firstnameTemp: String? = constantsController.getConstants(key:"firstname", namespace: "constants") + XCTAssertEqual(firstnameTemp, "jane") + + let favoriteColorTemp: String? = constantsController.getConstants(key: "favorite.color", namespace: "constants") + XCTAssertEqual(favoriteColorTemp, "blue") + + // Test fallback to original values when temporary values are not present + let lastnameTemp: String? = constantsController.getConstants(key: "lastname", namespace: "constants") + XCTAssertEqual(lastnameTemp, "doe") + + // Reset temp and values should be the same as the original data + constantsController.clearTemporaryValues(); + + let firstname: String? = constantsController.getConstants(key:"firstname", namespace: "constants") + XCTAssertEqual(firstname, "john") + + let favoriteColor: String? = constantsController.getConstants(key: "favorite.color", namespace: "constants") + XCTAssertEqual(favoriteColor, "red") + + let lastname: String? = constantsController.getConstants(key: "lastname", namespace: "constants") + XCTAssertEqual(lastname, "doe") + } } class FakePlugin: JSBasePlugin, NativePlugin { diff --git a/ios/swiftui/Sources/SwiftUIPlayer.swift b/ios/swiftui/Sources/SwiftUIPlayer.swift index d80c288ec..415c03792 100644 --- a/ios/swiftui/Sources/SwiftUIPlayer.swift +++ b/ios/swiftui/Sources/SwiftUIPlayer.swift @@ -205,6 +205,7 @@ public struct SwiftUIPlayer: View, HeadlessPlayer { public var body: some View { bodyContent .environment(\.inProgressState, (state as? InProgressState)) + .environment(\.constantsController, constantsController) // forward results from our Context along to our result binding .onReceive(context.$result.debounce(for: 0.1, scheduler: RunLoop.main)) { self.result = $0 @@ -238,12 +239,23 @@ struct InProgressStateKey: EnvironmentKey { static var defaultValue: InProgressState? } +/// EnvironmentKey for storing `constantsController` +struct ConstantsControllerStateKey: EnvironmentKey { + /// The default value for `@Environment(\.constantsController)` + static var defaultValue: ConstantsController? = nil +} + public extension EnvironmentValues { /// The `InProgressState` of Player if it is in progress, and in scope var inProgressState: InProgressState? { get { self[InProgressStateKey.self] } set { self[InProgressStateKey.self] = newValue } } + + var constantsController: ConstantsController? { + get { self[ConstantsControllerStateKey.self] } + set { self[ConstantsControllerStateKey.self] = newValue } + } } internal extension SwiftUIPlayer {