diff --git a/ios/core/Sources/Player/HeadlessPlayer.swift b/ios/core/Sources/Player/HeadlessPlayer.swift index 3399f340a..22b8be28a 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 @@ -169,6 +171,16 @@ public extension HeadlessPlayer { else { return nil } return BaseFlowState.createInstance(value: jsState) } + + /// The constants and context for player + 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..cbd74c854 --- /dev/null +++ b/ios/core/Sources/Types/Core/ConstantsController.swift @@ -0,0 +1,46 @@ +import JavaScriptCore + +public class ConstantsController { + var constantsController: JSValue? + + /// Function to retrieve constants from the providers store + /// - Parameters: + /// - key: Key used for the store access + /// - namespace: Namespace values were loaded under + /// - fallback:Optional - if key doesn't exist in namespace what to return (will return unknown if not provided) + /// - Returns: Constant values from store + 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?.toObject() as? T + } else { + let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace]) + return value?.toObject() as? T + } + } + + /// Function to add constants to the providers store + /// - Parameters: + /// - data: Values to add to the constants store + /// - namespace: Namespace values to be added under + public func addConstants(data: Any, namespace: String) -> Void { + self.constantsController?.invokeMethod("addConstants", withArguments: [data, namespace]) + } + + /// Function to set values to temporarily override certain keys in the perminant store + /// - Parameters: + /// - data: Values to override store with + /// - namespace: Namespace to override + public func setTemporaryValues(data: Any, namespace: String) -> Void { + self.constantsController?.invokeMethod("setTemporaryValues", withArguments: [data, namespace]) + } + + /// Clears any temporary values that were previously set + 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..26b4062e9 100644 --- a/ios/core/Tests/HeadlessPlayerTests.swift +++ b/ios/core/Tests/HeadlessPlayerTests.swift @@ -272,6 +272,105 @@ 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" + ], + "age": 1 + ] + + 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") + XCTAssertNil(middleName) + + 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 age: Int? = constantsController.getConstants(key:"age", namespace: "constants") + XCTAssertEqual(age, 1) + + let nonExistantNamespace: String? = constantsController.getConstants(key:"test", namespace: "foo") + XCTAssertNil(nonExistantNamespace) + + 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 fb7e66d9f..2be8973d4 100644 --- a/ios/swiftui/Sources/SwiftUIPlayer.swift +++ b/ios/swiftui/Sources/SwiftUIPlayer.swift @@ -199,6 +199,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 @@ -232,12 +233,24 @@ 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 } } + + /// The ConstantsController reference of Player + var constantsController: ConstantsController? { + get { self[ConstantsControllerStateKey.self] } + set { self[ConstantsControllerStateKey.self] = newValue } + } } internal extension SwiftUIPlayer {