+ {/* Scrollable on narrow screens so every tab stays reachable instead of
+ overflowing or wrapping. */}
+
({ label: tab.label }))}
active={activeLabel}
variant={TabListVariant.Bordered}
+ autoScrollActive
onClick={(label) => {
const tab = givebackTabs.find((item) => item.label === label);
if (tab) {
diff --git a/packages/shared/src/features/giveback/hooks/useGivebackCauseSelection.spec.tsx b/packages/shared/src/features/giveback/hooks/useGivebackCauseSelection.spec.tsx
index cff63557362..cb5cec86e7a 100644
--- a/packages/shared/src/features/giveback/hooks/useGivebackCauseSelection.spec.tsx
+++ b/packages/shared/src/features/giveback/hooks/useGivebackCauseSelection.spec.tsx
@@ -121,3 +121,44 @@ it('toasts a generic error and reports failure when saving fails', async () => {
expect(displayToast).toHaveBeenCalledWith(labels.error.generic);
expect(saved).toBe(false);
});
+
+it('toggleAndSave persists the new selection immediately with no toast', async () => {
+ const { result } = renderHook(() => useGivebackCauseSelection(true));
+
+ act(() => result.current.toggleAndSave('c1'));
+
+ expect(result.current.selectedIds.has('c1')).toBe(true);
+ await waitFor(() =>
+ expect(saveCausePreferences).toHaveBeenCalledWith(['c1']),
+ );
+ // The cause moving between sections is the feedback; no success toast.
+ expect(displayToast).not.toHaveBeenCalled();
+});
+
+it('toggleAndSave chains back-to-back toggles from the freshest set', async () => {
+ const { result } = renderHook(() => useGivebackCauseSelection(true));
+
+ // Two toggles in the same tick must not persist from a stale snapshot.
+ act(() => {
+ result.current.toggleAndSave('c1');
+ result.current.toggleAndSave('c2');
+ });
+
+ expect(result.current.selectedIds.has('c1')).toBe(true);
+ expect(result.current.selectedIds.has('c2')).toBe(true);
+ await waitFor(() =>
+ expect(saveCausePreferences).toHaveBeenLastCalledWith(['c1', 'c2']),
+ );
+});
+
+it('toggleAndSave rolls back the optimistic change when the save fails', async () => {
+ saveCausePreferences.mockRejectedValueOnce(new Error('network'));
+ const { result } = renderHook(() => useGivebackCauseSelection(true));
+
+ await act(async () => {
+ result.current.toggleAndSave('c1');
+ });
+
+ await waitFor(() => expect(result.current.selectedIds.has('c1')).toBe(false));
+ expect(displayToast).toHaveBeenCalledWith(labels.error.generic);
+});
diff --git a/packages/shared/src/features/giveback/hooks/useGivebackCauseSelection.ts b/packages/shared/src/features/giveback/hooks/useGivebackCauseSelection.ts
index 3c9e779548c..8a9f76dafe0 100644
--- a/packages/shared/src/features/giveback/hooks/useGivebackCauseSelection.ts
+++ b/packages/shared/src/features/giveback/hooks/useGivebackCauseSelection.ts
@@ -10,6 +10,9 @@ interface UseGivebackCauseSelection {
isLoading: boolean;
selectedIds: Set;
toggleCause: (id: string) => void;
+ // Toggle and persist immediately (no working-set/Save step). Used by the
+ // manage tab; the funnel uses toggleCause + an explicit save instead.
+ toggleAndSave: (id: string) => void;
selectedCount: number;
// Whether the visitor has confirmed causes before (drives the onboarded view).
hasSavedCauses: boolean;
@@ -30,6 +33,10 @@ export const useGivebackCauseSelection = (
useUpdateContributionCausePreferences();
const [selectedIds, setSelectedIds] = useState>(new Set());
+ // Mirror of the committed selection so toggleAndSave reads the freshest set
+ // (and chains correctly across same-tick toggles) without a stale closure.
+ const selectedIdsRef = useRef(selectedIds);
+ selectedIdsRef.current = selectedIds;
// Seed from saved preferences once they resolve, so editing starts from the
// visitor's current selection without stomping later in-picker toggles. Wait
@@ -57,6 +64,31 @@ export const useGivebackCauseSelection = (
});
}, []);
+ // Persist on each toggle so the manage tab needs no "Save" step. The cause
+ // visibly moving between sections is the feedback, so no toast on success.
+ // Derive the next set from the committed state (not the closed-over value) so
+ // back-to-back toggles can't persist from a stale snapshot, and roll the
+ // optimistic change back if the save fails.
+ const toggleAndSave = useCallback(
+ (id: string) => {
+ const previous = selectedIdsRef.current;
+ const next = new Set(previous);
+ if (next.has(id)) {
+ next.delete(id);
+ } else {
+ next.add(id);
+ }
+ selectedIdsRef.current = next;
+ setSelectedIds(next);
+ saveCausePreferences([...next]).catch(() => {
+ displayToast(labels.error.generic);
+ selectedIdsRef.current = previous;
+ setSelectedIds(previous);
+ });
+ },
+ [saveCausePreferences, displayToast],
+ );
+
const save = useCallback(async () => {
try {
await saveCausePreferences([...selectedIds]);
@@ -73,6 +105,7 @@ export const useGivebackCauseSelection = (
isLoading: isPending,
selectedIds,
toggleCause,
+ toggleAndSave,
selectedCount: selectedIds.size,
hasSavedCauses: selectedCauseIds.length > 0,
save,
diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts
index 34cd556d014..af6e47d36e8 100644
--- a/packages/shared/src/lib/log.ts
+++ b/packages/shared/src/lib/log.ts
@@ -507,7 +507,6 @@ export enum LogEvent {
SubmitGivebackActionError = 'submit giveback action error',
ClickGivebackLoveAction = 'click giveback love action',
ClaimGivebackReward = 'claim giveback reward',
- ClickGivebackEditCauses = 'click giveback edit causes',
ClickGivebackCause = 'click giveback cause',
ClickGivebackFaq = 'click giveback faq',
// Daily homepage