feat: error boundaries, loading states, and /privacy page#27
Conversation
New shared UI primitives: - ErrorBoundary (class component) — catches unexpected render errors with "Try again" reset - Skeleton / SkeletonCard / SkeletonText — shimmer loading placeholders - DataError — inline error display with optional retry button (compact + full variants) Hook improvements: - useRecentTransactions: add error state (was silently swallowing failures) - useBuddies: add error state + expose refresh (was silently falling back to mock) - useBudget: propagate real error instead of always returning null Dashboard error handling: - Show compact DataError banner when useDashboardPulse reports API errors - Show DataError in recent transactions section on fetch failure - Wrap DashboardClient in ErrorBoundary - Wrap (app) layout <main> in ErrorBoundary for all authenticated pages Budget page: - Add try/catch + error state for Supabase queries - Show DataError with retry when budget load fails - Remove unused Save import Privacy policy: - /privacy — full PDPA-compliant privacy policy page - Covers data collection, AI processing, retention, user rights, contact info https://claude.ai/code/session_017cLXjZu6kkjXRf2BemMPpp
❌ Deploy Preview for bajet-buddy failed.
|
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
bajet-buddy | f363fd8 | May 30 2026, 04:30 PM |
There was a problem hiding this comment.
Code Review
This pull request introduces robust error handling and error boundaries across the application, including the addition of reusable DataError, ErrorBoundary, and Skeleton components, as well as updating several hooks (useBuddies, useBudget, useRecentTransactions) to track and return error states. It also adds a new Privacy Policy page. Feedback on these changes includes consolidating a redundant duplicate call to useDashboardPulse() in BehaviorDashboard.tsx, implementing an active flag in useBudget.ts to prevent race conditions on unmounted components, and adding an optional onReset callback prop to the ErrorBoundary component to allow proper state cleanup during retries.
| const pulse = useDashboardPulse(); | ||
| const name = useUserName(); | ||
| const { budget, gamification, loading: pulseLoading } = useDashboardPulse(); | ||
| const { transactions: recentTx, loading: txLoading } = | ||
| const { budget, gamification, loading: pulseLoading, errors: pulseErrors } = useDashboardPulse(); |
There was a problem hiding this comment.
The useDashboardPulse() hook is called twice in this component. The first call assigns the result to pulse (which is unused), and the second call destructures the properties. This is redundant and can cause duplicate side effects or unnecessary re-renders depending on the hook's implementation.
We should remove the unused pulse variable and the duplicate hook call.
| const pulse = useDashboardPulse(); | |
| const name = useUserName(); | |
| const { budget, gamification, loading: pulseLoading } = useDashboardPulse(); | |
| const { transactions: recentTx, loading: txLoading } = | |
| const { budget, gamification, loading: pulseLoading, errors: pulseErrors } = useDashboardPulse(); | |
| const name = useUserName(); | |
| const { budget, gamification, loading: pulseLoading, errors: pulseErrors } = useDashboardPulse(); |
| </div> | ||
|
|
||
| {/* API error banner — shown when budget/freeze/gamification fetches fail */} | ||
| {pulseErrors.length > 0 && ( |
| useEffect(() => { | ||
| if (guestSummary) return; | ||
|
|
||
| setFetchError(null); | ||
| getBudgetSummary() | ||
| .then((data) => setFetchedSummary(data as BudgetSummary)) | ||
| .catch(() => { | ||
| .catch((err) => { | ||
| setFetchError(err instanceof Error ? err.message : "Unable to load budget"); | ||
| setFetchedSummary(MOCK_SUMMARY); | ||
| }) | ||
| .finally(() => setFetchLoading(false)); | ||
| }, [guestSummary]); |
There was a problem hiding this comment.
There is a potential race condition and state update on an unmounted component if getBudgetSummary() resolves after the component has unmounted or if guestSummary changes rapidly.
We should use an active flag to ignore the promise resolution if the effect is cleaned up, matching the robust pattern already used in useRecentTransactions.ts.
useEffect(() => {
if (guestSummary) return;
let active = true;
setFetchError(null);
getBudgetSummary()
.then((data) => {
if (!active) return;
setFetchedSummary(data as BudgetSummary);
})
.catch((err) => {
if (!active) return;
setFetchError(err instanceof Error ? err.message : "Unable to load budget");
setFetchedSummary(MOCK_SUMMARY);
})
.finally(() => {
if (!active) return;
setFetchLoading(false);
});
return () => {
active = false;
};
}, [guestSummary]);| interface Props { | ||
| children: React.ReactNode; | ||
| fallback?: React.ReactNode; | ||
| onError?: (error: Error, info: React.ErrorInfo) => void; | ||
| } |
There was a problem hiding this comment.
Add an optional onReset callback prop to the ErrorBoundary component. This allows parent components to reset any error-causing state or perform cleanup actions when the user clicks the "Try again" button, preventing immediate re-throws of the same error.
| interface Props { | |
| children: React.ReactNode; | |
| fallback?: React.ReactNode; | |
| onError?: (error: Error, info: React.ErrorInfo) => void; | |
| } | |
| interface Props { | |
| children: React.ReactNode; | |
| fallback?: React.ReactNode; | |
| onError?: (error: Error, info: React.ErrorInfo) => void; | |
| onReset?: () => void; | |
| } |
| this.props.onError?.(error, info); | ||
| } | ||
|
|
||
| private reset = () => this.setState({ hasError: false, error: null }); |
- Remove duplicate useDashboardPulse() call in BehaviorDashboard (was polling API twice) - Guard pulseErrors.length with nullish check (pulseErrors && pulseErrors.length > 0) - Add onReset callback prop to ErrorBoundary so parents can clean up state on retry - Add active flag in useBudget useEffect to prevent state updates on unmounted component https://claude.ai/code/session_017cLXjZu6kkjXRf2BemMPpp
Summary
ErrorBoundary(React class component),Skeleton/SkeletonCard/SkeletonText(shimmer loading),DataError(inline error display with retry, compact + full variants)useRecentTransactions,useBuddies, anduseBudgetnow all expose anerrorfield — previously they swallowed failures silently or always returnednullDataErrorbanner when budget/freeze/gamification API calls fail;DataErrorin recent transactions section on fetch failure;DashboardClientwrapped inErrorBoundary<main>in(app)/layout.tsxwrapped inErrorBoundary— catches unexpected render errors on any authenticated pageSaveimport/privacy: PDPA-compliant privacy policy covering data collection, AI processing (Anthropic/DeepSeek), OCR, retention, user rights, and contact infoTest plan
DataErrorwith retry button appearsErrorBoundarycatches it and shows "Try again" card/privacy→ full policy page renders without TopBar/BottomNavhttps://claude.ai/code/session_017cLXjZu6kkjXRf2BemMPpp
Generated by Claude Code
Summary by cubic
Adds app-wide error handling with error boundaries and visible loading/error states, plus a new PDPA-compliant
/privacypage. Users now see skeletons and clear retry options instead of crashes or silent failures.New Features
ErrorBoundarywraps app<main>andDashboardClientto catch render errors with a “Try again” reset; supportsonResetfor cleanup.Skeleton,SkeletonCard, andSkeletonTextfor shimmer loading.DataErrorbanner/card with optional Retry (compact and full)./privacypage detailing data collection, AI processing (Anthropic/DeepSeek), OCR, retention, and user rights.Reliability
useRecentTransactions,useBuddies(addsrefresh),useBudget.Saveimport.useDashboardPulse()call, null-safe guard onpulseErrors, and active flag inuseBudgetto avoid setState on unmounted.Written for commit f363fd8. Summary will update on new commits.