Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions static/gsApp/views/amCheckout/components/volumeSliders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,17 @@ export function renderPerformanceHovercard() {
}

function VolumeSliders({
currentSliderValues,
checkoutTier,
activePlan,
organization,
formData,
subscription,
onReservedChange,
}: Pick<
StepProps,
| 'activePlan'
| 'checkoutTier'
| 'organization'
| 'onUpdate'
| 'formData'
| 'subscription'
'activePlan' | 'checkoutTier' | 'organization' | 'onUpdate' | 'subscription'
> & {
currentSliderValues: Partial<Record<DataCategory, number>>;
onReservedChange: (value: number, category: DataCategory) => void;
}) {
const renderPerformanceUnitDecoration = () => (
Expand Down Expand Up @@ -102,7 +98,7 @@ function VolumeSliders({
}

const eventBucket = utils.getBucket({
events: formData.reserved[category],
events: currentSliderValues[category],
buckets: activePlan.planCategories[category],
});

Expand Down Expand Up @@ -170,7 +166,7 @@ function VolumeSliders({
<SpaceBetweenGrid>
<VolumeAmount>
{formatReservedWithUnits(
formData.reserved[category] ?? null,
currentSliderValues[category] ?? null,
category,
{
isAbbreviated: !isByteCategory(category),
Expand Down Expand Up @@ -201,7 +197,7 @@ function VolumeSliders({
getPlanCategoryName({plan: activePlan, category})
)
}
value={formData.reserved[category] ?? ''}
value={currentSliderValues[category] ?? ''}
allowedValues={allowedValues}
onChange={value =>
defined(value) && typeof value === 'number'
Expand Down
14 changes: 10 additions & 4 deletions static/gsApp/views/amCheckout/steps/reserveAdditionalVolume.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,17 @@ function ReserveAdditionalVolume({
}).price > 0
)
);
const [reserved, setReserved] = useState<Partial<Record<DataCategory, number>>>(
formData.reserved
);
Comment on lines +51 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The component's local reserved state does not sync with the formData.reserved prop, causing stale UI data and incorrect price calculations when the plan changes.
Severity: MEDIUM

Suggested Fix

Add a useEffect hook in the ReserveAdditionalVolume component to update the local reserved state whenever the formData.reserved prop changes. For example: useEffect(() => { setReserved(formData.reserved); }, [formData.reserved]);

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: static/gsApp/views/amCheckout/steps/reserveAdditionalVolume.tsx#L51-L53

Potential issue: The `ReserveAdditionalVolume` component initializes its local
`reserved` state from the `formData.reserved` prop but fails to update it when the prop
changes. When a user modifies their plan, the parent component recalculates
`formData.reserved` and passes it down. However, the `ReserveAdditionalVolume`
component's local state remains stale. This causes the UI to display incorrect
reservation values and the `reservedVolumeTotal` calculation to be based on this
outdated state, resulting in an inaccurate price being shown to the user.

Did we get this right? 👍 / 👎 to inform future reviews.

const reservedVolumeTotal = useMemo(() => {
return Object.entries(formData.reserved).reduce((acc, [category, value]) => {
return Object.entries(reserved).reduce((acc, [category, value]) => {
const bucket = activePlan.planCategories?.[category as DataCategory]?.find(
b => b.events === value
);
return acc + (bucket?.price ?? 0);
}, 0);
}, [formData.reserved, activePlan]);
}, [reserved, activePlan]);

const handleReservedChange = useCallback(
(value: number, category: DataCategory) => {
Expand Down Expand Up @@ -137,9 +140,12 @@ function ReserveAdditionalVolume({
activePlan={activePlan}
organization={organization}
onUpdate={onUpdate}
formData={formData}
subscription={subscription}
onReservedChange={debouncedReservedChange}
onReservedChange={(newReserved, category) => {
setReserved(prev => ({...prev, [category]: newReserved}));
debouncedReservedChange(newReserved, category);
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rapid multi-slider changes cause UI/data desync

Medium Severity

When a user rapidly adjusts multiple different category sliders within 300ms, lodash debounce only fires for the last call, discarding earlier calls. The local reserved state captures all changes, but formData.reserved only receives the last category's update. This causes the UI to show slider values that were never persisted—the sliders display one value while formData holds another, and reserved is never re-synced from formData after initialization.

Additional Locations (1)

Fix in Cursor Fix in Web

currentSliderValues={reserved}
/>
</Stack>
)}
Expand Down
Loading