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
12 changes: 11 additions & 1 deletion src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const navItems = [
];

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const { merchant, token, logout } = useAuthStore();
const { merchant, token, logout, hasHydrated } = useAuthStore();
const router = useRouter();
const pathname = usePathname();
const [mobileNavOpen, setMobileNavOpen] = useState(false);
Expand All @@ -46,6 +46,16 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
setMobileNavOpen(false);
}, [pathname]);

// Show a neutral loading state while Zustand rehydrates from localStorage.
// This prevents both the blank-page flash and the premature redirect.
if (!hasHydrated) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50" aria-busy="true" aria-label="Loading">
<div className="w-8 h-8 rounded-full border-4 border-brand-200 border-t-brand-600 animate-spin" />
</div>
);
}

if (!merchant) return null;

const visibleNavItems = navItems.filter((item) => !item.adminOnly || isAdmin(merchant));
Expand Down
2 changes: 1 addition & 1 deletion src/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export default function SettingsPage() {
{ value: 'settlements:read', label: 'Settlements: read' },
{ value: 'webhooks:manage', label: 'Webhooks: manage' },
].map(({ value, label }) => (
<label key={value} className="flex items-center gap-2 mb-2 block text-sm text-gray-700">
<label key={value} className="flex items-center gap-2 mb-2 text-sm text-gray-700">
<input
type="checkbox"
className="form-checkbox"
Expand Down
5 changes: 5 additions & 0 deletions src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import type { Merchant } from './types';
interface AuthState {
token: string | null;
merchant: Merchant | null;
/** True once Zustand's persist middleware has finished rehydrating from localStorage. */
hasHydrated: boolean;
setAuth: (token: string, merchant: Merchant) => void;
logout: () => void;
_setHasHydrated: (value: boolean) => void;
}

const LEGACY_TOKEN_KEY = 'access_token';
Expand All @@ -28,6 +31,7 @@ export const useAuthStore = create<AuthState>()(
(set) => ({
token: null,
merchant: null,
hasHydrated: false,
setAuth: (token, merchant) => {
clearLegacyAccessTokenKey();
set({ token, merchant });
Expand All @@ -36,6 +40,7 @@ export const useAuthStore = create<AuthState>()(
clearLegacyAccessTokenKey();
set({ token: null, merchant: null });
},
_setHasHydrated: (value) => set({ hasHydrated: value }),
}),
{ name: 'dupdub-auth', onRehydrateStorage: () => () => clearLegacyAccessTokenKey() },
),
Expand Down
26 changes: 26 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ export function formatDate(date: string | Date | undefined | null): string {
}).format(d);
}

import {
Clock,
AlertCircle,
CheckCircle,
RefreshCw,
XCircle,
HelpCircle,
type LucideIcon,
} from 'lucide-react';

export const STATUS_COLORS: Record<string, string> = {
pending: 'bg-yellow-100 text-yellow-800',
pending_approval: 'bg-orange-100 text-orange-800',
Expand All @@ -42,6 +52,22 @@ export const STATUS_COLORS: Record<string, string> = {
expired: 'bg-gray-100 text-gray-800',
};

/**
* Distinguishing icon per status so that state is never conveyed by color alone.
* Use alongside STATUS_COLORS in badge/pill elements to meet WCAG 1.4.1 (Use of Color).
*/
export const STATUS_ICONS: Record<string, LucideIcon> = {
pending: Clock,
pending_approval: AlertCircle,
confirmed: CheckCircle,
processing: RefreshCw,
settling: RefreshCw,
settled: CheckCircle,
completed: CheckCircle,
failed: XCircle,
expired: HelpCircle,
};

/** @deprecated use STATUS_COLORS */
export const PAYMENT_STATUS_COLORS = STATUS_COLORS;

Expand Down