Skip to content

Commit bca866a

Browse files
committed
Use optimistic checkout redirect URL loading strategy to improve checkout load time
1 parent 0b58e58 commit bca866a

File tree

7 files changed

+355
-128
lines changed

7 files changed

+355
-128
lines changed

.changeset/cute-spies-talk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bigcommerce/catalyst-core": minor
3+
---
4+
5+
Use optimistic checkout redirect URL loading strategy to improve checkout load time
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use server';
2+
3+
import { getSessionCustomerAccessToken } from '~/auth';
4+
import { getChannelIdFromLocale } from '~/channels.config';
5+
import { client } from '~/client';
6+
import { graphql } from '~/client/graphql';
7+
import { getVisitIdCookie, getVisitorIdCookie } from '~/lib/analytics/bigcommerce';
8+
9+
const CheckoutRedirectMutation = graphql(`
10+
mutation CheckoutRedirectMutation($cartId: String!, $visitId: UUID, $visitorId: UUID) {
11+
cart {
12+
createCartRedirectUrls(
13+
input: { cartEntityId: $cartId, visitId: $visitId, visitorId: $visitorId }
14+
) {
15+
errors {
16+
... on NotFoundError {
17+
__typename
18+
}
19+
}
20+
redirectUrls {
21+
redirectedCheckoutUrl
22+
}
23+
}
24+
}
25+
}
26+
`);
27+
28+
export interface GenerateCheckoutUrlResult {
29+
url: string | null;
30+
error?: string;
31+
}
32+
33+
export async function generateCheckoutUrl(
34+
cartId: string,
35+
locale: string,
36+
): Promise<GenerateCheckoutUrlResult> {
37+
try {
38+
const customerAccessToken = await getSessionCustomerAccessToken();
39+
const channelId = getChannelIdFromLocale(locale);
40+
const visitId = await getVisitIdCookie();
41+
const visitorId = await getVisitorIdCookie();
42+
43+
const { data } = await client.fetch({
44+
document: CheckoutRedirectMutation,
45+
variables: { cartId, visitId, visitorId },
46+
fetchOptions: { cache: 'no-store' },
47+
customerAccessToken,
48+
channelId,
49+
});
50+
51+
if (
52+
data.cart.createCartRedirectUrls.errors.length > 0 ||
53+
!data.cart.createCartRedirectUrls.redirectUrls
54+
) {
55+
return { url: null, error: 'Failed to generate checkout URL' };
56+
}
57+
58+
return { url: data.cart.createCartRedirectUrls.redirectUrls.redirectedCheckoutUrl };
59+
} catch (error) {
60+
// eslint-disable-next-line no-console
61+
console.error('Error generating checkout URL:', error);
62+
63+
return { url: null, error: 'Failed to generate checkout URL' };
64+
}
65+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use client';
2+
3+
import { SubmissionResult } from '@conform-to/react';
4+
import { useEffect, useState } from 'react';
5+
6+
import {
7+
CartClient as CartComponent,
8+
CartLineItem,
9+
CartProps,
10+
} from '@/vibes/soul/sections/cart/client';
11+
import { useRouter } from '~/i18n/routing';
12+
import { isCheckoutUrlValid } from '~/lib/checkout-url-utils';
13+
14+
type CheckoutAction = (
15+
lastResult: SubmissionResult | null,
16+
formData: FormData,
17+
) => Promise<SubmissionResult | null>;
18+
19+
interface OptimizedCartProps<LineItem extends CartLineItem> {
20+
preGeneratedUrl: string | null;
21+
fallbackAction: CheckoutAction;
22+
cartProps: CartProps<LineItem>;
23+
}
24+
25+
export function OptimizedCart<LineItem extends CartLineItem>({
26+
preGeneratedUrl,
27+
fallbackAction,
28+
cartProps,
29+
}: OptimizedCartProps<LineItem>) {
30+
const router = useRouter();
31+
const [checkoutUrl, setCheckoutUrl] = useState<string | null>(preGeneratedUrl);
32+
const [pageLoadTime] = useState(() => Date.now()); // Capture when this component first mounted
33+
34+
// Update the stored URL if a new one is provided
35+
useEffect(() => {
36+
setCheckoutUrl(preGeneratedUrl);
37+
}, [preGeneratedUrl]);
38+
39+
// Custom action that checks URL validity before redirecting
40+
const optimizedCheckoutAction: CheckoutAction = async (lastResult, formData) => {
41+
// Check if we have a valid pre-generated URL (accounting for clock skew and page load time)
42+
if (checkoutUrl && isCheckoutUrlValid(checkoutUrl, pageLoadTime)) {
43+
// Direct client-side redirect for better performance
44+
router.push(checkoutUrl);
45+
46+
// Return null to prevent further processing
47+
return null;
48+
}
49+
50+
// Fall back to the original server action if URL is invalid or missing
51+
return fallbackAction(lastResult, formData);
52+
};
53+
54+
return <CartComponent {...cartProps} checkoutAction={optimizedCheckoutAction} />;
55+
}

0 commit comments

Comments
 (0)