Skip to content

Commit 9bf5de1

Browse files
feat(web): seed permission sync banner with server-side initial state
Refactors getPermissionSyncStatus into a reusable server action and passes the result as initialData to the banner query, eliminating the loading flash on first render. Also fixes hasPendingFirstSync to treat accounts with no sync jobs yet as pending. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 564480d commit 9bf5de1

3 files changed

Lines changed: 49 additions & 25 deletions

File tree

packages/web/src/app/[domain]/components/permissionSyncBanner.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { usePrevious } from "@uidotdev/usehooks";
1111

1212
const POLL_INTERVAL_MS = 5000;
1313

14-
export function PermissionSyncBanner() {
14+
interface PermissionSyncBannerProps {
15+
initialHasPendingFirstSync: boolean;
16+
}
17+
18+
export function PermissionSyncBanner({ initialHasPendingFirstSync }: PermissionSyncBannerProps) {
1519
const router = useRouter();
1620

1721
const { data: hasPendingFirstSync, isError, isPending } = useQuery({
@@ -25,6 +29,9 @@ export function PermissionSyncBanner() {
2529
// Keep polling while sync is in progress, stop when done
2630
return hasPendingFirstSync ? POLL_INTERVAL_MS : false;
2731
},
32+
initialData: {
33+
hasPendingFirstSync: initialHasPendingFirstSync,
34+
}
2835
});
2936

3037
const previousHasPendingFirstSync = usePrevious(hasPendingFirstSync);

packages/web/src/app/[domain]/layout.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { UpgradeToast } from "./components/upgradeToast";
2626
import { getLinkedAccountProviderStates } from "@/ee/features/permissionSyncing/actions";
2727
import { LinkAccounts } from "@/ee/features/permissionSyncing/components/linkAccounts";
2828
import { PermissionSyncBanner } from "./components/permissionSyncBanner";
29+
import { getPermissionSyncStatus } from "../api/(server)/ee/permissionSyncStatus/route";
2930

3031
interface LayoutProps {
3132
children: React.ReactNode,
@@ -190,12 +191,18 @@ export default async function Layout(props: LayoutProps) {
190191
)
191192
}
192193
const isPermissionSyncBannerVisible = session && hasEntitlement("permission-syncing");
194+
const hasPendingFirstSync = isPermissionSyncBannerVisible ? (await getPermissionSyncStatus()) : null;
193195

194196
return (
195197
<SyntaxGuideProvider>
196198
{
197199
isPermissionSyncBannerVisible ? (
198-
<PermissionSyncBanner />
200+
<PermissionSyncBanner
201+
initialHasPendingFirstSync={(isServiceError(hasPendingFirstSync) || hasPendingFirstSync === null) ?
202+
false :
203+
hasPendingFirstSync.hasPendingFirstSync
204+
}
205+
/>
199206
) : null
200207
}
201208
{children}
Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
'use server';
22

33
import { apiHandler } from "@/lib/apiHandler";
4-
import { serviceErrorResponse } from "@/lib/serviceError";
4+
import { ServiceError, serviceErrorResponse } from "@/lib/serviceError";
55
import { isServiceError } from "@/lib/utils";
66
import { withAuthV2 } from "@/withAuthV2";
7-
import { getEntitlements } from "@sourcebot/shared";
7+
import { env, getEntitlements } from "@sourcebot/shared";
88
import { AccountPermissionSyncJobStatus } from "@sourcebot/db";
99
import { StatusCodes } from "http-status-codes";
1010
import { ErrorCode } from "@/lib/errorCodes";
11+
import { sew } from "@/actions";
1112

1213
export interface PermissionSyncStatusResponse {
1314
hasPendingFirstSync: boolean;
@@ -18,16 +19,28 @@ export interface PermissionSyncStatusResponse {
1819
* synced for the first time.
1920
*/
2021
export const GET = apiHandler(async () => {
21-
const entitlements = getEntitlements();
22-
if (!entitlements.includes('permission-syncing')) {
23-
return serviceErrorResponse({
24-
statusCode: StatusCodes.FORBIDDEN,
25-
errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS,
26-
message: "Permission syncing is not enabled for your license",
27-
});
22+
const result = await getPermissionSyncStatus();
23+
24+
if (isServiceError(result)) {
25+
return serviceErrorResponse(result);
2826
}
2927

30-
const result = await withAuthV2(async ({ prisma, user }) => {
28+
return Response.json(result, { status: StatusCodes.OK });
29+
});
30+
31+
32+
export const getPermissionSyncStatus = async (): Promise<PermissionSyncStatusResponse | ServiceError> => sew(async () =>
33+
withAuthV2(async ({ prisma, user }) => {
34+
const entitlements = getEntitlements();
35+
if (!entitlements.includes('permission-syncing')) {
36+
return {
37+
statusCode: StatusCodes.FORBIDDEN,
38+
errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS,
39+
message: "Permission syncing is not enabled for your license",
40+
} satisfies ServiceError;
41+
}
42+
43+
3144
const accounts = await prisma.account.findMany({
3245
where: {
3346
userId: user.id,
@@ -46,18 +59,15 @@ export const GET = apiHandler(async () => {
4659
AccountPermissionSyncJobStatus.IN_PROGRESS
4760
];
4861

49-
const hasPendingFirstSync = accounts.some(account =>
50-
account.permissionSyncedAt === null &&
51-
account.permissionSyncJobs.length > 0 &&
52-
activeStatuses.includes(account.permissionSyncJobs[0].status)
53-
);
62+
const hasPendingFirstSync = env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' &&
63+
accounts.some(account =>
64+
account.permissionSyncedAt === null &&
65+
// @note: to handle the case where the permission sync job
66+
// has not yet been scheduled for a new account, we consider
67+
// accounts with no permission sync jobs as having a pending first sync.
68+
(account.permissionSyncJobs.length === 0 || (account.permissionSyncJobs.length > 0 && activeStatuses.includes(account.permissionSyncJobs[0].status)))
69+
)
5470

5571
return { hasPendingFirstSync } satisfies PermissionSyncStatusResponse;
56-
});
57-
58-
if (isServiceError(result)) {
59-
return serviceErrorResponse(result);
60-
}
61-
62-
return Response.json(result, { status: StatusCodes.OK });
63-
});
72+
})
73+
)

0 commit comments

Comments
 (0)