coder 4
2026-09-29 12:26:59 -06:00
committed by bb-expert
parent f4460aff97
commit 2fcfc23800
4 changed files with 121 additions and 7 deletions

View File

@@ -692,6 +692,7 @@ function CommunityApp({
<EnterpriseBrowserLoginGate
communityName={enterpriseLoginGate.communityName}
error={enterpriseLoginGate.error}
isPending={enterpriseLoginGate.isPending}
onCancel={() => {
enterpriseLoginGate.onCancel();
if (transaction) void handleCommunityOnboardingCancel();

View File

@@ -7,6 +7,7 @@ import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
type EnterpriseBrowserLoginGateProps = {
communityName: string;
error?: string | null;
isPending?: boolean;
onCancel: () => void;
onContinue: () => void;
};
@@ -14,6 +15,7 @@ type EnterpriseBrowserLoginGateProps = {
export function EnterpriseBrowserLoginGate({
communityName,
error,
isPending = false,
onCancel,
onContinue,
}: EnterpriseBrowserLoginGateProps) {
@@ -33,9 +35,9 @@ export function EnterpriseBrowserLoginGate({
Sign in with your company account
</h1>
<p className="mt-3 text-sm leading-6 text-muted-foreground">
{communityName} requires enterprise sign-in. Buzz will open your
system browser so your identity provider can handle SSO, passkeys, and
device-trust checks outside the app.
{isPending
? "Finish sign-in in your browser, then return to Buzz. If the browser flow was closed or is stuck, cancel here to recover."
: `${communityName} requires enterprise sign-in. Buzz will open your system browser so your identity provider can handle SSO, passkeys, and device-trust checks outside the app.`}
</p>
{error ? (
<p
@@ -49,10 +51,11 @@ export function EnterpriseBrowserLoginGate({
<Button
className="h-10 w-full"
data-testid="enterprise-browser-login-continue"
disabled={isPending}
onClick={onContinue}
type="button"
>
Continue in browser
{isPending ? "Waiting for browser sign-in…" : "Continue in browser"}
</Button>
<Button
className="h-10 w-full"
@@ -61,7 +64,7 @@ export function EnterpriseBrowserLoginGate({
type="button"
variant="secondary"
>
Cancel
{isPending ? "Cancel sign-in" : "Cancel"}
</Button>
</div>
</div>

View File

@@ -301,6 +301,88 @@ test("useCommunityInit cancels an owned pending enterprise authentication login
}
});
test("useCommunityInit keeps cancellation visible while browser enterprise login is pending", async () => {
const { cleanup, renderHook, waitFor, act } = await import(
"@testing-library/react"
);
const { useCommunityInit } = await import("./useCommunityInit.ts");
const calls = [];
let rejectLogin;
const restore = installTauriInvoke(async (command, args) => {
calls.push([command, args]);
if (command === "get_identity") {
return { pubkey: "pubkey-1", display_name: "Tester" };
}
if (command === "set_agent_avatar_communities") return null;
if (command === "enterprise_login_gate") return { status: "required" };
if (command === "get_enterprise_auth") return null;
if (command === "start_enterprise_auth_login") {
return new Promise((_, reject) => {
rejectLogin = reject;
});
}
if (command === "cancel_enterprise_auth_login") return null;
if (command === "apply_workspace") {
throw new Error("apply_workspace must not run while login is pending");
}
throw new Error(`unexpected command: ${command}`);
});
try {
const hook = renderHook(() =>
useCommunityInit(testCommunity(), "community-key", false, true),
);
await waitFor(() => assert.ok("enterpriseLogin" in hook.result.current));
assert.equal(hook.result.current.enterpriseLogin.isPending, false);
await act(async () => {
hook.result.current.enterpriseLogin.onContinue();
});
await waitFor(() => {
assert.equal(
calls.some(([command]) => command === "start_enterprise_auth_login"),
true,
);
});
assert.ok("enterpriseLogin" in hook.result.current);
assert.equal(hook.result.current.enterpriseLogin.isPending, true);
assert.equal(hook.result.current.enterpriseLogin.error, null);
await act(async () => {
hook.result.current.enterpriseLogin.onCancel();
rejectLogin(new Error("Enterprise sign-in canceled"));
});
await waitFor(() =>
assert.deepEqual(
calls.find(([command]) => command === "cancel_enterprise_auth_login"),
[
"cancel_enterprise_auth_login",
{
attemptId: calls.find(
([command]) => command === "start_enterprise_auth_login",
)[1].attemptId,
},
],
),
);
await waitFor(() =>
assert.equal(hook.result.current.error, "Enterprise sign-in canceled"),
);
assert.equal(
calls.some(([command]) => command === "apply_workspace"),
false,
);
hook.unmount();
} finally {
restore();
cleanup();
mock.reset();
}
});
test("useCommunityInit waits for explicit enterprise browser consent", async () => {
const { cleanup, renderHook, waitFor, act } = await import(
"@testing-library/react"

View File

@@ -137,6 +137,7 @@ type CommunityInitResult =
enterpriseLogin: {
communityName: string;
error: string | null;
isPending: boolean;
onCancel: () => void;
onContinue: () => void;
};
@@ -175,15 +176,19 @@ export function useCommunityInit(
const [enterpriseLoginPrompt, setEnterpriseLoginPrompt] = useState<{
communityName: string;
error: string | null;
isPending: boolean;
} | null>(null);
const enterpriseLoginDecisionRef = useRef<
((allowed: boolean) => void) | null
>(null);
const enterpriseLoginAttemptIdRef = useRef<string | null>(null);
const continueEnterpriseLogin = useCallback(() => {
const resolve = enterpriseLoginDecisionRef.current;
enterpriseLoginDecisionRef.current = null;
setEnterpriseLoginPrompt(null);
setEnterpriseLoginPrompt((prompt) =>
prompt ? { ...prompt, error: null, isPending: true } : prompt,
);
resolve?.(true);
}, []);
@@ -191,6 +196,13 @@ export function useCommunityInit(
const resolve = enterpriseLoginDecisionRef.current;
enterpriseLoginDecisionRef.current = null;
setEnterpriseLoginPrompt(null);
const attemptId = enterpriseLoginAttemptIdRef.current;
if (attemptId !== null) {
void cancelEnterpriseAuthLogin({ attemptId }).catch(() => {
// Best-effort cleanup for a browser login this hook invocation owns.
});
enterpriseLoginAttemptIdRef.current = null;
}
resolve?.(false);
}, []);
@@ -408,19 +420,28 @@ export function useCommunityInit(
setEnterpriseLoginPrompt({
communityName: activeCommunity.name,
error: null,
isPending: false,
});
});
},
onBrowserLoginStarted: () => {
ownedEnterpriseLoginAttemptId = enterpriseLoginAttemptId;
enterpriseLoginAttemptIdRef.current = enterpriseLoginAttemptId;
},
},
);
enterpriseProfileForResult =
authoritativeEnterpriseProfile(enterpriseAuth);
ownedEnterpriseLoginAttemptId = null;
if (enterpriseLoginAttemptIdRef.current === enterpriseLoginAttemptId) {
enterpriseLoginAttemptIdRef.current = null;
}
setEnterpriseLoginPrompt(null);
} catch (error) {
ownedEnterpriseLoginAttemptId = null;
if (enterpriseLoginAttemptIdRef.current === enterpriseLoginAttemptId) {
enterpriseLoginAttemptIdRef.current = null;
}
const errorMessage =
error instanceof Error
? error.message
@@ -428,7 +449,9 @@ export function useCommunityInit(
console.error("Enterprise login gate failed:", error);
if (!cancelled) {
setEnterpriseLoginPrompt((prompt) =>
prompt ? { ...prompt, error: errorMessage } : prompt,
prompt
? { ...prompt, error: errorMessage, isPending: false }
: prompt,
);
setResult({
isReady: false,
@@ -546,6 +569,11 @@ export function useCommunityInit(
}).catch(() => {
// Best-effort cleanup for a browser login this hook invocation owns.
});
if (
enterpriseLoginAttemptIdRef.current === ownedEnterpriseLoginAttemptId
) {
enterpriseLoginAttemptIdRef.current = null;
}
ownedEnterpriseLoginAttemptId = null;
}
};