mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
Fence enterprise retry results by init attempt
Signed-off-by: coder 0 <d97ebdbb198c7237c94f84ea8bb8a73583ea067407eebd0062abbb3962527fb1@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:
@@ -429,9 +429,15 @@ function CommunityApp({
|
||||
const isCommunitySwitch = hasSwitchedCommunityRef.current;
|
||||
const isContinuingOnboarding = continueOnboarding && !isCommunitySwitch;
|
||||
|
||||
const transaction = communityOnboarding.transaction;
|
||||
const communityInitAttempt =
|
||||
transaction && transaction.communityId === activeCommunity?.id
|
||||
? (transaction.initAttempt ?? 0)
|
||||
: -1;
|
||||
const community = useCommunityInit(
|
||||
activeCommunity,
|
||||
communityKey,
|
||||
communityInitAttempt,
|
||||
sharedIdentity,
|
||||
isFindingCommunityAfterLeave,
|
||||
communities,
|
||||
@@ -536,7 +542,6 @@ function CommunityApp({
|
||||
|
||||
const bootSplashPhase = useBootSplashHold();
|
||||
|
||||
const transaction = communityOnboarding.transaction;
|
||||
useEffect(() => {
|
||||
if (transaction?.stage !== "connecting") {
|
||||
connectingTransactionRef.current = null;
|
||||
@@ -546,7 +551,8 @@ function CommunityApp({
|
||||
const targetIsReady =
|
||||
transaction?.communityId === activeCommunity?.id &&
|
||||
community.isReady &&
|
||||
community.appliedKey === communityKey;
|
||||
community.appliedKey === communityKey &&
|
||||
community.initAttempt === communityInitAttempt;
|
||||
const enterpriseProfile = community.isReady
|
||||
? community.enterpriseProfile
|
||||
: null;
|
||||
@@ -554,6 +560,8 @@ function CommunityApp({
|
||||
useEffect(() => {
|
||||
if (
|
||||
transaction?.stage !== "connecting" ||
|
||||
!("initAttempt" in community) ||
|
||||
community.initAttempt !== communityInitAttempt ||
|
||||
!("appliedKey" in community) ||
|
||||
community.appliedKey !== communityKey ||
|
||||
!("error" in community) ||
|
||||
@@ -565,7 +573,13 @@ function CommunityApp({
|
||||
{ stage: "corporate-profile", error: community.error },
|
||||
transaction.id,
|
||||
);
|
||||
}, [community, communityKey, communityOnboarding, transaction]);
|
||||
}, [
|
||||
community,
|
||||
communityInitAttempt,
|
||||
communityKey,
|
||||
communityOnboarding,
|
||||
transaction,
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (transaction?.stage !== "connecting" || !targetIsReady) return;
|
||||
const transactionId = transaction.id;
|
||||
|
||||
@@ -93,9 +93,12 @@ function neverSettles() {
|
||||
}
|
||||
|
||||
function mount(communities) {
|
||||
return renderHook((list) => useCommunityInit(b, "b", false, false, list), {
|
||||
initialProps: communities,
|
||||
});
|
||||
return renderHook(
|
||||
(list) => useCommunityInit(b, "b", null, false, false, list),
|
||||
{
|
||||
initialProps: communities,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
test("useCommunityInit gates enterprise login before applying the community", async () => {
|
||||
@@ -122,7 +125,7 @@ test("useCommunityInit gates enterprise login before applying the community", as
|
||||
try {
|
||||
const community = testCommunity();
|
||||
const hook = renderHook(() =>
|
||||
useCommunityInit(community, "community-key", false, true),
|
||||
useCommunityInit(community, "community-key", null, false, true),
|
||||
);
|
||||
|
||||
await waitFor(() => assert.equal(hook.result.current.isReady, true));
|
||||
@@ -182,7 +185,7 @@ test("useCommunityInit blocks community apply when enterprise login gate fails",
|
||||
|
||||
try {
|
||||
const hook = renderHook(() =>
|
||||
useCommunityInit(testCommunity(), "community-key", false, true),
|
||||
useCommunityInit(testCommunity(), "community-key", null, false, true),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
@@ -244,7 +247,8 @@ test("useCommunityInit cancels an owned pending enterprise authentication login
|
||||
|
||||
try {
|
||||
const hook = renderHook(
|
||||
({ community, key }) => useCommunityInit(community, key, false, true),
|
||||
({ community, key }) =>
|
||||
useCommunityInit(community, key, null, false, true),
|
||||
{ initialProps: { community: communityA, key: "community-a-key" } },
|
||||
);
|
||||
|
||||
@@ -321,7 +325,7 @@ test("useCommunityInit waits for explicit enterprise browser consent", async ()
|
||||
try {
|
||||
const community = testCommunity({ name: "Block Buzz" });
|
||||
const hook = renderHook(() =>
|
||||
useCommunityInit(community, "community-key", false, true),
|
||||
useCommunityInit(community, "community-key", null, false, true),
|
||||
);
|
||||
|
||||
await waitFor(() => assert.ok("enterpriseLogin" in hook.result.current));
|
||||
@@ -392,7 +396,7 @@ test("useCommunityInit exposes authoritative enterprise profile when both identi
|
||||
|
||||
try {
|
||||
const hook = renderHook(() =>
|
||||
useCommunityInit(testCommunity(), "community-key", false, true),
|
||||
useCommunityInit(testCommunity(), "community-key", null, false, true),
|
||||
);
|
||||
await waitFor(() => assert.ok("enterpriseLogin" in hook.result.current));
|
||||
await act(async () => {
|
||||
|
||||
@@ -114,6 +114,7 @@ type CommunityInitResult =
|
||||
isReady: true;
|
||||
needsSetup: false;
|
||||
appliedKey: string;
|
||||
initAttempt: number | null;
|
||||
identityPubkey: string | null;
|
||||
enterpriseProfile: EnterpriseProfileSeed | null;
|
||||
}
|
||||
@@ -122,11 +123,17 @@ type CommunityInitResult =
|
||||
needsSetup: true;
|
||||
defaultRelayUrl: string;
|
||||
}
|
||||
| { isReady: false; needsSetup: false; appliedKey: string | null }
|
||||
| {
|
||||
isReady: false;
|
||||
needsSetup: false;
|
||||
appliedKey: string | null;
|
||||
initAttempt: number | null;
|
||||
}
|
||||
| {
|
||||
isReady: false;
|
||||
needsSetup: false;
|
||||
appliedKey: string | null;
|
||||
initAttempt: number | null;
|
||||
enterpriseLogin: {
|
||||
communityName: string;
|
||||
error: string | null;
|
||||
@@ -138,6 +145,7 @@ type CommunityInitResult =
|
||||
isReady: false;
|
||||
needsSetup: false;
|
||||
appliedKey: string | null;
|
||||
initAttempt: number | null;
|
||||
error: string;
|
||||
};
|
||||
|
||||
@@ -152,6 +160,7 @@ type CommunityInitResult =
|
||||
export function useCommunityInit(
|
||||
activeCommunity: Community | null,
|
||||
communityKey: string,
|
||||
initAttempt: number | null,
|
||||
isSharedIdentity: boolean,
|
||||
suppressAutoConnect = false,
|
||||
communities: readonly Community[] = [],
|
||||
@@ -161,6 +170,7 @@ export function useCommunityInit(
|
||||
isReady: false,
|
||||
needsSetup: false,
|
||||
appliedKey: null,
|
||||
initAttempt: null,
|
||||
});
|
||||
const [enterpriseLoginPrompt, setEnterpriseLoginPrompt] = useState<{
|
||||
communityName: string;
|
||||
@@ -200,6 +210,7 @@ export function useCommunityInit(
|
||||
isReady: false,
|
||||
needsSetup: false,
|
||||
appliedKey: null,
|
||||
initAttempt: null,
|
||||
error: "Could not refresh avatar source permissions. Reload to retry.",
|
||||
});
|
||||
});
|
||||
@@ -242,6 +253,7 @@ export function useCommunityInit(
|
||||
isReady: false,
|
||||
needsSetup: false,
|
||||
appliedKey: null,
|
||||
initAttempt: null,
|
||||
error:
|
||||
error instanceof Error
|
||||
? `Could not safely leave community: ${error.message}`
|
||||
@@ -317,11 +329,14 @@ export function useCommunityInit(
|
||||
// Mark this community config as pending while it is applied to the
|
||||
// backend. App.tsx also checks appliedKey against the active communityKey,
|
||||
// which prevents rendering community-scoped UI for a new community until
|
||||
// that exact config has finished applying.
|
||||
// that exact config has finished applying. The init attempt is captured
|
||||
// from the visible onboarding transaction so retry generations can fence
|
||||
// stale completions for the same community config.
|
||||
setResult({
|
||||
isReady: false,
|
||||
needsSetup: false,
|
||||
appliedKey: communityKey,
|
||||
initAttempt,
|
||||
});
|
||||
|
||||
// Resolve the active signer before resetting singletons so
|
||||
@@ -367,6 +382,7 @@ export function useCommunityInit(
|
||||
isReady: false,
|
||||
needsSetup: false,
|
||||
appliedKey: null,
|
||||
initAttempt: null,
|
||||
error:
|
||||
error instanceof Error
|
||||
? `Could not safely switch communities: ${error.message}`
|
||||
@@ -418,6 +434,7 @@ export function useCommunityInit(
|
||||
isReady: false,
|
||||
needsSetup: false,
|
||||
appliedKey: communityKey,
|
||||
initAttempt,
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
@@ -470,6 +487,7 @@ export function useCommunityInit(
|
||||
isReady: false,
|
||||
needsSetup: false,
|
||||
appliedKey: null,
|
||||
initAttempt: null,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
@@ -506,6 +524,7 @@ export function useCommunityInit(
|
||||
isReady: true,
|
||||
needsSetup: false,
|
||||
appliedKey: communityKey,
|
||||
initAttempt,
|
||||
identityPubkey,
|
||||
enterpriseProfile: enterpriseProfileForResult,
|
||||
});
|
||||
@@ -538,6 +557,7 @@ export function useCommunityInit(
|
||||
isSharedIdentity,
|
||||
suppressAutoConnect,
|
||||
communityKey,
|
||||
initAttempt,
|
||||
]);
|
||||
|
||||
if (enterpriseLoginPrompt !== null) {
|
||||
@@ -545,6 +565,7 @@ export function useCommunityInit(
|
||||
isReady: false,
|
||||
needsSetup: false,
|
||||
appliedKey: activeCommunity ? communityKey : null,
|
||||
initAttempt: activeCommunity ? initAttempt : null,
|
||||
enterpriseLogin: {
|
||||
...enterpriseLoginPrompt,
|
||||
onCancel: cancelEnterpriseLogin,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
isTransactionStillConnecting,
|
||||
loadCommunityOnboardingTransaction,
|
||||
markCommunityOnboardingComplete,
|
||||
nextCommunityOnboardingInitAttempt,
|
||||
resolveProfileCheckAction,
|
||||
shouldSkipCommunityOnboarding,
|
||||
startCommunityOnboarding,
|
||||
@@ -130,6 +131,48 @@ test("acknowledgment persists but resets when the same-relay link reopens", () =
|
||||
assert.equal(reopened.acknowledged, undefined);
|
||||
});
|
||||
|
||||
test("retry generation advances before stale same-community results can match", () => {
|
||||
const storage = createMemoryStorage();
|
||||
const transaction = startCommunityOnboarding(
|
||||
{ source: "first-community", relayUrl: "wss://relay.example" },
|
||||
storage,
|
||||
);
|
||||
assert.equal(transaction.initAttempt, 0);
|
||||
|
||||
const retryAttempt = nextCommunityOnboardingInitAttempt(transaction);
|
||||
const retried = updateCommunityOnboardingTransaction(
|
||||
transaction,
|
||||
{ stage: "connecting", initAttempt: retryAttempt, error: undefined },
|
||||
storage,
|
||||
);
|
||||
|
||||
assert.equal(retryAttempt, 1);
|
||||
assert.equal(retried.initAttempt, 1);
|
||||
assert.equal(loadCommunityOnboardingTransaction(storage)?.initAttempt, 1);
|
||||
assert.notEqual(retried.initAttempt, transaction.initAttempt);
|
||||
});
|
||||
|
||||
test("same-relay ingress resumes with a fresh init generation", () => {
|
||||
const storage = createMemoryStorage();
|
||||
const transaction = startCommunityOnboarding(
|
||||
{ source: "deep-link-connect", relayUrl: "wss://relay.example" },
|
||||
storage,
|
||||
);
|
||||
updateCommunityOnboardingTransaction(
|
||||
transaction,
|
||||
{ stage: "corporate-profile", error: "Browser login was rejected" },
|
||||
storage,
|
||||
);
|
||||
|
||||
const reopened = startCommunityOnboarding(
|
||||
{ source: "deep-link-connect", relayUrl: "wss://relay.example" },
|
||||
storage,
|
||||
);
|
||||
|
||||
assert.equal(reopened.id, transaction.id);
|
||||
assert.equal(reopened.initAttempt, 1);
|
||||
assert.equal(reopened.error, undefined);
|
||||
});
|
||||
test("malformed persisted state is ignored and can be cleared", () => {
|
||||
const storage = createMemoryStorage({
|
||||
"buzz-community-onboarding-transaction.v1": '{"stage":"profile"}',
|
||||
|
||||
@@ -36,6 +36,12 @@ export type CommunityOnboardingTransaction = {
|
||||
/** First-run screen that launched this transaction, restored on cancel. */
|
||||
firstCommunityPage?: FirstCommunityPage;
|
||||
stage: CommunityOnboardingStage;
|
||||
/**
|
||||
* Frontend-owned init generation. Incremented before a retry becomes a
|
||||
* visible connecting transaction so late results from the previous init
|
||||
* cannot mutate the retried transaction.
|
||||
*/
|
||||
initAttempt?: number;
|
||||
relayUrl: string;
|
||||
inviteCode?: string;
|
||||
communityName: string;
|
||||
@@ -62,6 +68,7 @@ export type CommunityOnboardingTransactionPatch = Partial<
|
||||
Pick<
|
||||
CommunityOnboardingTransaction,
|
||||
| "stage"
|
||||
| "initAttempt"
|
||||
| "relayUrl"
|
||||
| "communityId"
|
||||
| "previousCommunityId"
|
||||
@@ -167,6 +174,7 @@ export function startCommunityOnboarding(
|
||||
policyReceipt: input.policyReceipt ?? existing.policyReceipt,
|
||||
updatedAt: now.toISOString(),
|
||||
error: undefined,
|
||||
initAttempt: (existing.initAttempt ?? 0) + 1,
|
||||
// A freshly opened link deserves fresh feedback — re-present the gate
|
||||
// even if a previous link for this relay was already dismissed.
|
||||
acknowledged: undefined,
|
||||
@@ -189,11 +197,18 @@ export function startCommunityOnboarding(
|
||||
policyReceipt: input.policyReceipt,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
initAttempt: 0,
|
||||
};
|
||||
saveCommunityOnboardingTransaction(transaction, storage);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
export function nextCommunityOnboardingInitAttempt(
|
||||
transaction: CommunityOnboardingTransaction,
|
||||
): number {
|
||||
return (transaction.initAttempt ?? 0) + 1;
|
||||
}
|
||||
|
||||
export function updateCommunityOnboardingTransaction(
|
||||
transaction: CommunityOnboardingTransaction,
|
||||
patch: CommunityOnboardingTransactionPatch,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Plus, Users } from "lucide-react";
|
||||
|
||||
import {
|
||||
markCommunityOnboardingComplete,
|
||||
nextCommunityOnboardingInitAttempt,
|
||||
type CommunityOnboardingStage,
|
||||
useCommunityOnboarding,
|
||||
} from "@/features/onboarding/communityOnboarding";
|
||||
@@ -274,11 +275,10 @@ export function CommunityOnboardingFlow({
|
||||
}, [clear, isEnteringStage]);
|
||||
|
||||
const retry = () => {
|
||||
if (!transaction) return;
|
||||
update({
|
||||
stage: nextRetryStage(
|
||||
transaction?.stage,
|
||||
Boolean(transaction?.inviteCode),
|
||||
),
|
||||
stage: nextRetryStage(transaction.stage, Boolean(transaction.inviteCode)),
|
||||
initAttempt: nextCommunityOnboardingInitAttempt(transaction),
|
||||
error: undefined,
|
||||
});
|
||||
onRetry?.();
|
||||
|
||||
Reference in New Issue
Block a user