Skip to content

Commit 71cc747

Browse files
authored
fix(client): preserve FTUE auth onboarding (#711)
1 parent 41dd012 commit 71cc747

10 files changed

+517
-503
lines changed

.changeset/fix-ftue-auth-gating.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@frontman-ai/client": patch
3+
---
4+
5+
Preserve the initial FTUE state during ACP authentication so first-time users still see onboarding instead of being treated as returning users after other client preferences are persisted.

libs/client/src/Client__ConnectionReducer.res

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type state = {
5252
acp: acpState,
5353
relay: relayState,
5454
session: sessionState,
55+
initialAuthBehavior: Client__FtueState.authBehavior,
5556
isSendingPrompt: bool,
5657
// Relay instance exists before connection completes - needed for MCPServer
5758
relayInstance: option<Relay.t>,
@@ -112,7 +113,11 @@ type action =
112113
type effect =
113114
| LogError(string)
114115
| LogInfo(string)
115-
| ConnectACP({config: ACP.config, signal: WebAPI.EventAPI.abortSignal})
116+
| ConnectACP({
117+
config: ACP.config,
118+
signal: WebAPI.EventAPI.abortSignal,
119+
initialAuthBehavior: Client__FtueState.authBehavior,
120+
})
116121
| ConnectRelay(Relay.t)
117122
| DisconnectRelay(Relay.t)
118123
| DisconnectACP(ACP.connection)
@@ -151,6 +156,7 @@ let initialState: state = {
151156
acp: ACPDisconnected,
152157
relay: RelayDisconnected,
153158
session: NoSession,
159+
initialAuthBehavior: Client__FtueState.RedirectToLogin,
154160
isSendingPrompt: false,
155161
relayInstance: None,
156162
mcpServer: None,
@@ -248,13 +254,18 @@ let reduce = (state: state, action: action): (state, array<effect>) => {
248254
acp: ACPConnecting,
249255
relay: RelayConnecting,
250256
session: NoSession,
257+
initialAuthBehavior: state.initialAuthBehavior,
251258
isSendingPrompt: false,
252259
relayInstance: Some(relay),
253260
mcpServer: Some(mcpServer),
254261
abortController: Some(abortController),
255262
},
256263
[
257-
ConnectACP({config: acpConfig, signal: abortController.signal}),
264+
ConnectACP({
265+
config: acpConfig,
266+
signal: abortController.signal,
267+
initialAuthBehavior: state.initialAuthBehavior,
268+
}),
258269
ConnectRelay(relay),
259270
LogInfo("Initializing connections..."),
260271
],
@@ -441,7 +452,7 @@ let reduce = (state: state, action: action): (state, array<effect>) => {
441452
| ACPConnected(conn) => [DisconnectACP(conn)]
442453
| _ => []
443454
}
444-
(initialState, Array.flat([abortEffects, relayEffects, acpEffects]))
455+
({...initialState, initialAuthBehavior: state.initialAuthBehavior}, Array.flat([abortEffects, relayEffects, acpEffects]))
445456

446457
// === Invalid transitions ===
447458
| (_, Initialize(_)) => (
@@ -516,7 +527,7 @@ let handleEffect = (effect: effect, state: state, dispatch: action => unit) => {
516527
| AbortConnections(controller) =>
517528
Log.info("Aborting in-flight connections")
518529
WebAPI.AbortController.abort(controller)
519-
| ConnectACP({config, signal}) =>
530+
| ConnectACP({config, signal, initialAuthBehavior}) =>
520531
let connect = async () => {
521532
let result = await ACP.connect(config, ~signal)
522533
switch result {
@@ -533,12 +544,10 @@ let handleEffect = (effect: effect, state: state, dispatch: action => unit) => {
533544
WebAPI.Global.window->WebAPI.Window.location->WebAPI.Location.href
534545
let returnTo = encodeURIComponent(currentUrl)
535546
let fullUrl = `${loginUrl}?return_to=${returnTo}`
536-
// For first-time users, surface the auth URL so the UI can show a welcome modal.
537-
// Returning users get redirected immediately.
538-
switch Client__FtueState.get() {
539-
| Client__FtueState.New =>
547+
switch initialAuthBehavior {
548+
| Client__FtueState.ShowWelcomeModal =>
540549
dispatch(ACPConnectError(`auth_required:${fullUrl}`))
541-
| Client__FtueState.WelcomeShown | Client__FtueState.Completed =>
550+
| Client__FtueState.RedirectToLogin =>
542551
WebAPI.Global.window->WebAPI.Window.location->WebAPI.Location.assign(fullUrl)
543552
}
544553
| ACP.ConnectionFailed(msg) => dispatch(ACPConnectError(msg))

libs/client/src/Client__FrontmanProvider.res

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,11 @@ module Provider = {
190190
})
191191

192192
// Use StateReducer - effects are executed in useEffect, not during dispatch
193-
let (state, dispatch) = StateReducer.useReducer(module(Reducer), Reducer.initialState)
193+
let initialConnectionState = {
194+
...Reducer.initialState,
195+
initialAuthBehavior: Client__FtueState.getAuthBehavior(),
196+
}
197+
let (state, dispatch) = StateReducer.useReducer(module(Reducer), initialConnectionState)
194198

195199
// Single initialization effect
196200
React.useEffect0(() => {

libs/client/src/Client__FtueState.res

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ type t =
1616
| WelcomeShown
1717
| Completed
1818

19+
type authBehavior =
20+
| ShowWelcomeModal
21+
| RedirectToLogin
22+
1923
// Check whether any other frontman:* localStorage key exists, indicating a returning user
2024
let hasExistingFrontmanData = (): bool => {
2125
try {
@@ -57,6 +61,13 @@ let get = (): t => {
5761
}
5862
}
5963

64+
let getAuthBehavior = (): authBehavior => {
65+
switch get() {
66+
| New => ShowWelcomeModal
67+
| WelcomeShown | Completed => RedirectToLogin
68+
}
69+
}
70+
6071
let setWelcomeShown = () => {
6172
FrontmanBindings.LocalStorage.setItem(storageKey, "welcome_shown")
6273
}

libs/client/src/styles/frontman-theme.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,3 @@
262262
.font-ibm-plex-mono [data-code-block-header] span {
263263
margin-left: 0.375rem;
264264
}
265-

libs/client/test/Client__ConnectionReducer.test.res

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Reducer = Client__ConnectionReducer
44
module ACP = FrontmanAiFrontmanClient.FrontmanClient__ACP
55
module Relay = FrontmanAiFrontmanClient.FrontmanClient__Relay
66
module MCPServer = FrontmanAiFrontmanClient.FrontmanClient__MCP__Server
7+
module FtueState = Client__FtueState
78

89
// Helper to check if effect list contains a specific effect type
910
let hasEffect = (effects, predicate) => effects->Array.some(predicate)
@@ -35,6 +36,13 @@ let hasConnectRelay = effects =>
3536
| _ => false
3637
}
3738
)
39+
let getConnectACPInitialAuthBehavior = effects =>
40+
effects->Array.findMap(e =>
41+
switch e {
42+
| Reducer.ConnectACP({initialAuthBehavior}) => Some(initialAuthBehavior)
43+
| _ => None
44+
}
45+
)
3846

3947
describe("Connection Reducer", () => {
4048
describe("Initial State", () => {
@@ -82,7 +90,7 @@ describe("Connection Reducer", () => {
8290
_meta: JSON.Encode.object(Dict.fromArray([("framework", JSON.Encode.string("test"))])),
8391
}
8492
let (nextState, effects) = Reducer.reduce(
85-
Reducer.initialState,
93+
{...Reducer.initialState, initialAuthBehavior: FtueState.ShowWelcomeModal},
8694
Initialize({config: mockConfig, relay: mockRelay, mcpServer: mockServer}),
8795
)
8896

@@ -91,6 +99,7 @@ describe("Connection Reducer", () => {
9199
t->expect(Option.isSome(nextState.relayInstance))->Expect.toBe(true)
92100
t->expect(Option.isSome(nextState.mcpServer))->Expect.toBe(true)
93101
t->expect(hasConnectACP(effects))->Expect.toBe(true)
102+
t->expect(getConnectACPInitialAuthBehavior(effects))->Expect.toBe(Some(FtueState.ShowWelcomeModal))
94103
t->expect(hasConnectRelay(effects))->Expect.toBe(true)
95104
})
96105

@@ -403,6 +412,7 @@ describe("Connection Reducer", () => {
403412
acp: ACPConnected(mockConn),
404413
relay: RelayConnected,
405414
session: SessionActive(mockSession),
415+
initialAuthBehavior: FtueState.ShowWelcomeModal,
406416
isSendingPrompt: false,
407417
relayInstance: Some(mockRelay),
408418
mcpServer: Some(mockServer),

0 commit comments

Comments
 (0)