mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
Keep enterprise login recovery controls visible
Signed-off-by: coder 4 <925bd99f26e4411be456977d98977dafe4feafee0e02ef9277b4875dfc29af04@buzz.block.builderlab.xyz> Signed-off-by: Brainy Bumble <607666932e190ac5b35e80f42080da5e94b6b256c1bc6ab5e5b083d68393bdea@buzz.block.builderlab.xyz> Signed-off-by: bb-expert <51cde9442e46eac81c83ec71552708c3b0cb96a88ff8e1581d8fb4de281afbf3@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -692,6 +692,7 @@ function CommunityApp({
|
||||
<EnterpriseBrowserLoginGate
|
||||
communityName={enterpriseLoginGate.communityName}
|
||||
error={enterpriseLoginGate.error}
|
||||
isPending={enterpriseLoginGate.isPending}
|
||||
onCancel={() => {
|
||||
enterpriseLoginGate.onCancel();
|
||||
if (transaction) void handleCommunityOnboardingCancel();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user