Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion ios/core/Sources/Types/Core/Flow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,19 @@ public class Flow: CreatedFromJSValue {
*/
public init(_ value: JSValue) {
self.value = value
hooks = FlowHooks(transition: Hook2(baseValue: value, name: "transition"), afterTransition: Hook(baseValue: value, name: "afterTransition"))
hooks = FlowHooks(
beforeTransition: WaterfallHook2(baseValue: value, name: "beforeTransition"),
transition: Hook2(baseValue: value, name: "transition"),
afterTransition: Hook(baseValue: value, name: "afterTransition")
)
}
}

public struct FlowHooks {
/// A hook that fires before a transition, with the current state and transition value. The handler receives
/// (state as JSValue, transitionValue as String) and must return the state as JSValue (pass-through or modified).
public var beforeTransition: WaterfallHook2

/// A hook that fires when transitioning states and giving the old and new states as parameters
public var transition: Hook2<NamedState?, NamedState>

Expand Down
3 changes: 3 additions & 0 deletions ios/core/Sources/Types/Core/NavigationStates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ open class NavigationBaseState: CreatedFromJSValue {

internal let rawValue: JSValue

/// The backing JSValue. Use when returning this state from a waterfall hook (e.g. beforeTransition).
public var jsValue: JSValue { rawValue }

Comment on lines +13 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be public?

public static func createInstance(value: JSValue) -> NavigationBaseState {
let base = NavigationBaseState(value)
switch base.stateType {
Expand Down
25 changes: 25 additions & 0 deletions ios/core/Sources/Types/Hooks/Hook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,31 @@ public class Hook2<T, U>: BaseJSHook where T: CreatedFromJSValue, U: CreatedFrom
}
}

/**
A waterfall hook with 2 parameters. The handler receives (state, transitionValue) and must return
the state so the JS runtime can pass it to the next tap or use it as the result.
Used for flow hooks like beforeTransition where the return value is the (possibly modified) state.
Aligns with FlowHooks.transition and afterTransition: use tap { state, transitionValue in ... return state }.
*/
public class WaterfallHook2: BaseJSHook {
/**
Attach a closure to the hook. When the hook is fired in the JS runtime, the handler receives
the current state as NavigationBaseState? and the transition value as String. Return the state
(pass-through or modified), or nil to pass through the original state.
- parameters:
- hook: A function (state, transitionValue) -> state (or nil to pass through)
*/
public func tap(_ hook: @escaping (NavigationBaseState?, String) -> NavigationBaseState?) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make this generic so it’s reusable and aligned with web’s SyncWaterfallHook?
have you tried using SyncWaterfallHook2JS<T, R, U> from this PR where T = first arg (e.g. state), U = second arg (e.g. transition name or options), R = return type (typically same as T for waterfall)?

let tapMethod: @convention(block) (JSValue?, JSValue?) -> JSValue? = { value, value2 in
guard let val = value, let val2 = value2 else { return nil }
let transitionValue = val2.toString() ?? ""
let typedState = NavigationBaseState.createInstance(value: val)
return hook(typedState, transitionValue)?.jsValue ?? val
}
Comment on lines +126 to +131
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can we do guard let value and keep the original names instead of shadowing the names? I think that's a little cleaner. Also, can we do value1 and value2 to keep the names consistent? E.g.

let tapMethod: @convention(block) (JSValue?, JSValue?) -> JSValue? = { value1, value2 in
    guard let value1, let value2 else { return nil }
    let transitionValue = value2.toString() ?? ""
    let typedState = NavigationBaseState.createInstance(value: value1)
    return hook(typedState, transitionValue)?.jsValue ?? value1
}

self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any])
}
}

/**
This class represents an object in the JS runtime that can be tapped into
to receive JS events
Expand Down
50 changes: 50 additions & 0 deletions ios/core/Tests/FlowStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,54 @@ class FlowStateTests: XCTestCase {
wait(for: [endStateHit], timeout: 1)

}

/// Mirrors JVM FlowControllerIntegrationTest: beforeTransition, transition, and afterTransition hooks fire as expected.
func testFlowControllerBeforeTransitionHook() {
let player = HeadlessPlayerImpl(plugins: [])
var pendingTransition: (NamedState?, String)?
var completedTransition: (NamedState?, NamedState)?
var flowInstanceAfterTransition: Flow?

player.hooks?.flowController.tap { flowController in
flowController.hooks.flow.tap { flow in
flow.hooks.beforeTransition.tap { state, transitionValue in
pendingTransition = (flow.currentState, transitionValue)
return state
}
flow.hooks.transition.tap { from, to in
if pendingTransition?.0?.name != from?.name { pendingTransition = nil }
completedTransition = (from, to)
}
flow.hooks.afterTransition.tap { flowInstance in
flowInstanceAfterTransition = flowInstance
}
}
}

let inProgress = expectation(description: "in progress")
player.hooks?.state.tap { newState in
if newState is InProgressState { inProgress.fulfill() }
}

player.start(flow: FlowData.COUNTER) { _ in }

wait(for: [inProgress], timeout: 2)

XCTAssertNil(pendingTransition)
XCTAssertNil(completedTransition?.0)
XCTAssertEqual((completedTransition?.1)?.name, "VIEW_1")

do {
try (player.state as? InProgressState)?.controllers?.flow.transition(with: "*")
} catch {
XCTFail("Transition failed: \(error)")
}

let pendingFrom = pendingTransition?.0?.value as? NavigationFlowViewState
XCTAssertEqual(pendingFrom?.ref, "action")
XCTAssertEqual(pendingTransition?.1, "*")
XCTAssertEqual(completedTransition?.0?.name, "VIEW_1")
XCTAssertEqual((completedTransition?.1)?.name, "END_Done")
XCTAssertNotNil(flowInstanceAfterTransition)
}
}