Skip to content

Commit 2e38ff5

Browse files
committed
Prerendering detection & handle differential versions
1 parent b1f25d8 commit 2e38ff5

File tree

9 files changed

+645
-7
lines changed

9 files changed

+645
-7
lines changed

packages/core/src/app/checkout/CheckoutPage.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import type CheckoutStepStatus from './CheckoutStepStatus';
5959
import CheckoutStepType from './CheckoutStepType';
6060
import type CheckoutSupport from './CheckoutSupport';
6161
import mapToCheckoutProps from './mapToCheckoutProps';
62+
import { type CheckoutRefreshAPI, createCheckoutRefreshAPI, createPrerenderingChangeHandler, PrerenderingStalenessDetector } from './prerenderingStalenessDetector';
6263

6364
const Billing = lazy(() =>
6465
retry(
@@ -181,12 +182,22 @@ class Checkout extends Component<
181182

182183
private embeddedMessenger?: EmbeddedCheckoutMessenger;
183184
private unsubscribeFromConsignments?: () => void;
185+
private prerenderingStalenessDetector = new PrerenderingStalenessDetector();
186+
private checkoutRefreshAPI?: CheckoutRefreshAPI;
184187

185188
componentWillUnmount(): void {
186189
if (this.unsubscribeFromConsignments) {
187190
this.unsubscribeFromConsignments();
188191
this.unsubscribeFromConsignments = undefined;
189192
}
193+
194+
// Clean up prerendering staleness detector
195+
this.prerenderingStalenessDetector.reset();
196+
197+
// Clean up global API
198+
if (typeof window !== 'undefined' && (window as any).checkoutRefreshAPI === this.checkoutRefreshAPI) {
199+
delete (window as any).checkoutRefreshAPI;
200+
}
190201

191202
window.removeEventListener('beforeunload', this.handleBeforeExit);
192203
this.handleBeforeExit();
@@ -195,11 +206,13 @@ class Checkout extends Component<
195206
async componentDidMount(): Promise<void> {
196207
const {
197208
analyticsTracker,
209+
checkoutId,
198210
containerId,
199211
createEmbeddedMessenger,
200212
data,
201213
embeddedStylesheet,
202214
extensionService,
215+
loadCheckout,
203216
loadPaymentMethodByIds,
204217
subscribeToConsignments,
205218
} = this.props;
@@ -248,14 +261,47 @@ class Checkout extends Component<
248261
messenger.postLoaded();
249262

250263
if (document.prerendering) {
251-
document.addEventListener('prerenderingchange', () => {
264+
// Capture initial snapshot when page is prerendered
265+
this.prerenderingStalenessDetector.captureInitialSnapshot(data);
266+
267+
// Set up enhanced prerenderingchange handler
268+
const prerenderingChangeHandler = createPrerenderingChangeHandler(
269+
this.prerenderingStalenessDetector,
270+
loadCheckout,
271+
() => data,
272+
checkoutId,
273+
(wasStale) => {
274+
// Log for debugging - this could be removed in production
275+
if (wasStale) {
276+
// eslint-disable-next-line no-console
277+
console.debug('Checkout data was refreshed due to staleness after prerendering');
278+
}
279+
}
280+
);
281+
282+
document.addEventListener('prerenderingchange', async () => {
252283
analyticsTracker.checkoutBegin();
284+
// Refresh checkout data in background if needed
285+
await prerenderingChangeHandler();
253286
}, { once: true });
254287
}
255288
else {
256289
analyticsTracker.checkoutBegin();
257290
}
258291

292+
// Set up global checkout refresh API
293+
this.checkoutRefreshAPI = createCheckoutRefreshAPI(
294+
this.prerenderingStalenessDetector,
295+
loadCheckout,
296+
() => data,
297+
checkoutId
298+
);
299+
300+
// Expose API globally for external use
301+
if (typeof window !== 'undefined') {
302+
(window as any).checkoutRefreshAPI = this.checkoutRefreshAPI;
303+
}
304+
259305
const consignments = data.getConsignments();
260306
const cart = data.getCart();
261307

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Prerendering Staleness Detection
2+
3+
This document describes the implementation of staleness detection for prerendered checkout pages.
4+
5+
## Overview
6+
7+
When using prerendering with Speculation Rules, checkout pages may become stale if the cart state changes after prerendering but before the user navigates to the checkout. This implementation automatically detects and refreshes stale checkout data without causing jank for users.
8+
9+
## Implementation
10+
11+
### Core Components
12+
13+
1. **PrerenderingStalenessDetector**: Captures and compares checkout snapshots
14+
2. **Prerendering Change Handler**: Handles the `prerenderingchange` event
15+
3. **Global Refresh API**: Exposed on `window.checkoutRefreshAPI` for external use
16+
17+
### Detection Strategy
18+
19+
The system compares these key identifiers to detect staleness:
20+
21+
- Cart ID
22+
- Cart updated time
23+
- Cart item count (physical + digital + gift certificates)
24+
- Cart base amount
25+
- Checkout ID
26+
- Number of consignments
27+
28+
### Usage
29+
30+
#### Automatic Detection (Built-in)
31+
32+
The implementation automatically:
33+
34+
1. Captures initial snapshot when a page is prerendered
35+
2. Listens for the `prerenderingchange` event
36+
3. Refreshes checkout data in the background if changes are detected
37+
4. Logs when refresh occurs due to staleness
38+
39+
#### Manual Refresh API
40+
41+
The global API is available at `window.checkoutRefreshAPI`:
42+
43+
```typescript
44+
// Check if checkout data is stale
45+
const isStale = window.checkoutRefreshAPI?.isCheckoutStale();
46+
47+
// Force refresh checkout data
48+
const result = await window.checkoutRefreshAPI?.refreshCheckout(true);
49+
console.log('Refresh success:', result.success, 'Was stale:', result.wasStale);
50+
51+
// Get current snapshot for debugging
52+
const snapshot = window.checkoutRefreshAPI?.getCurrentSnapshot();
53+
console.log('Current checkout state:', snapshot);
54+
```
55+
56+
#### External Integration Examples
57+
58+
```javascript
59+
// Example: Refresh when cart is modified in another tab
60+
window.addEventListener('storage', async (e) => {
61+
if (e.key === 'cart_modified') {
62+
const result = await window.checkoutRefreshAPI?.refreshCheckout();
63+
if (result?.wasStale) {
64+
console.log('Checkout was refreshed due to cart changes');
65+
}
66+
}
67+
});
68+
69+
// Example: Periodic staleness check
70+
setInterval(async () => {
71+
if (window.checkoutRefreshAPI?.isCheckoutStale()) {
72+
await window.checkoutRefreshAPI.refreshCheckout();
73+
}
74+
}, 30000); // Check every 30 seconds
75+
```
76+
77+
## Files Modified
78+
79+
- `packages/core/src/app/checkout/prerenderingStalenessDetector.ts` - Core implementation
80+
- `packages/core/src/app/checkout/CheckoutPage.tsx` - Integration with checkout page
81+
- `packages/core/types/dom.extended.d.ts` - Type definitions for global API
82+
83+
## Benefits
84+
85+
1. **Seamless UX**: No jank when checkout data hasn't changed
86+
2. **Automatic Updates**: Stale data is refreshed transparently
87+
3. **External API**: Allows custom refresh triggers from external code
88+
4. **Debugging Support**: Comprehensive logging and inspection capabilities
89+
90+
## Edge Cases Handled
91+
92+
- Missing cart/checkout data
93+
- Network failures during refresh (graceful degradation)
94+
- Multiple rapid refresh attempts
95+
- Component unmounting during refresh
96+
- API cleanup on page navigation
97+
98+
## Testing
99+
100+
Comprehensive test coverage includes:
101+
- Staleness detection accuracy
102+
- Error handling
103+
- API functionality
104+
- Integration with existing prerendering flow
105+
106+
Run tests with:
107+
```bash
108+
npx jest packages/core/src/app/checkout/prerenderingStalenessDetector.test.ts --config=packages/core/jest.config.js
109+
```

0 commit comments

Comments
 (0)