diff --git a/app/hooks/useStoreOwnership.ts b/app/hooks/useStoreOwnership.ts index 48d10ad..09c475b 100644 --- a/app/hooks/useStoreOwnership.ts +++ b/app/hooks/useStoreOwnership.ts @@ -38,3 +38,21 @@ export function useStoreOwnership(storeId: string | undefined, mode?: string) { stores: stores ?? [], }; } + +/** + * Guard for OWNER-ONLY store pages (financial, shipping, edit, team). Redirects + * non-members (via useStoreOwnership) and staff members back to the dashboard. + * Returns { isLoading, isOwner } so callers can render a spinner meanwhile. + */ +export function useRequireOwner(storeId: string | undefined) { + const navigate = useNavigate(); + const { isLoading, role } = useStoreOwnership(storeId); + + useEffect(() => { + if (!isLoading && role && role !== "owner") { + navigate(`/store/${storeId}`); + } + }, [isLoading, role, storeId, navigate]); + + return { isLoading, isOwner: role === "owner" }; +} diff --git a/app/requestHandler/use-seller-hooks.ts b/app/requestHandler/use-seller-hooks.ts index 8cf39a9..c0df4e7 100644 --- a/app/requestHandler/use-seller-hooks.ts +++ b/app/requestHandler/use-seller-hooks.ts @@ -45,10 +45,14 @@ export function useMyStores() { const res = await fetch(`${getApiBaseUrl()}/api/seller/my-stores/`, { headers: { Authorization: `Bearer ${token}` }, }); - if (!res.ok) return []; + // Throw on failure so a transient error stays an ERROR state (data kept, + // retried) instead of resolving to [] — otherwise a blip would look like + // "no stores" and bounce an owner off their own dashboard. + if (!res.ok) throw new Error(`my-stores failed: ${res.status}`); return res.json(); }, enabled: !!token, + retry: 1, staleTime: 60 * 1000, }); } diff --git a/app/routes/store.$storeId.financial-dashboard._index.tsx b/app/routes/store.$storeId.financial-dashboard._index.tsx index ca26018..96fa2a3 100644 --- a/app/routes/store.$storeId.financial-dashboard._index.tsx +++ b/app/routes/store.$storeId.financial-dashboard._index.tsx @@ -8,6 +8,7 @@ import { HandCoins, } from "lucide-react"; import HeaderWithSupport from "../components/HeaderWithSupport"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; export const meta: MetaFunction = () => { return [ @@ -41,6 +42,7 @@ export const meta: MetaFunction = () => { export default function FinancialDashboard() { const { storeId } = useParams(); + useRequireOwner(storeId); // financial is owner-only const navigate = useNavigate(); const handleBack = () => { safeGoBack(navigate); diff --git a/app/routes/store.$storeId.financial-dashboard.cash-funds.tsx b/app/routes/store.$storeId.financial-dashboard.cash-funds.tsx index c89f7ce..f497b22 100644 --- a/app/routes/store.$storeId.financial-dashboard.cash-funds.tsx +++ b/app/routes/store.$storeId.financial-dashboard.cash-funds.tsx @@ -16,8 +16,9 @@ import { CarouselItem, } from "../components/ui/carousel"; import { useCallback, useState, useRef, useEffect } from "react"; -import { useNavigate } from "@remix-run/react"; +import { useNavigate, useParams } from "@remix-run/react"; import { useCarousel } from "../hooks/useCarousel"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; import { Drawer, DrawerTitle, @@ -50,6 +51,8 @@ import { BankLogo } from "~/components/BankLogo"; export default function CashingFunds() { const navigate = useNavigate(); + const { storeId } = useParams(); + useRequireOwner(storeId); // financial is owner-only const handleBack = () => { safeGoBack(navigate); }; diff --git a/app/routes/store.$storeId.financial-dashboard.reports.tsx b/app/routes/store.$storeId.financial-dashboard.reports.tsx index 8c2820b..3f6e8f0 100644 --- a/app/routes/store.$storeId.financial-dashboard.reports.tsx +++ b/app/routes/store.$storeId.financial-dashboard.reports.tsx @@ -2,14 +2,17 @@ import { Sun, Waves, Loader2 } from "lucide-react"; import HeaderWithSupport from "../components/HeaderWithSupport"; import PersianDatePicker from "../components/PersianDatePicker"; import { useEffect, useState } from "react"; -import { useSearchParams, useNavigate } from "react-router-dom"; +import { useSearchParams, useNavigate, useParams } from "react-router-dom"; import { formatPrice, safeGoBack } from "../utils/helpers"; import { useServiceGetReport } from "../requestHandler/use-wallet-hooks"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; import jMoment from "moment-jalaali"; import { MetaFunction } from "@remix-run/node"; export default function Reports() { const navigate = useNavigate(); + const { storeId } = useParams(); + useRequireOwner(storeId); // financial is owner-only const handleBack = () => { safeGoBack(navigate); }; diff --git a/app/routes/store.$storeId.financial-dashboard.transactions.tsx b/app/routes/store.$storeId.financial-dashboard.transactions.tsx index bcf874e..364cbf9 100644 --- a/app/routes/store.$storeId.financial-dashboard.transactions.tsx +++ b/app/routes/store.$storeId.financial-dashboard.transactions.tsx @@ -12,8 +12,9 @@ import { } from "../components/ui/drawer"; import { RadioGroup, RadioGroupItem } from "../components/ui/radio-group"; import PersianDatePicker from "../components/PersianDatePicker"; -import { useSearchParams } from "react-router-dom"; +import { useSearchParams, useParams } from "react-router-dom"; import { useServiceGetPayoutRequests } from "../requestHandler/use-wallet-hooks"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; import jMoment from "moment-jalaali"; import filterIcon from "~/assets/icons/filter.png"; import filterIconDark from "~/assets/icons/filter-dark.png"; @@ -51,6 +52,8 @@ export const meta: MetaFunction = () => { export default function Transactions() { const navigate = useNavigate(); + const { storeId } = useParams(); + useRequireOwner(storeId); // financial is owner-only const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); const [searchParams] = useSearchParams(); const { theme } = useRootData(); diff --git a/app/routes/store.$storeId.shipping-method.tsx b/app/routes/store.$storeId.shipping-method.tsx index e1e5b41..ff8bc51 100644 --- a/app/routes/store.$storeId.shipping-method.tsx +++ b/app/routes/store.$storeId.shipping-method.tsx @@ -15,7 +15,7 @@ import { import { SellerCourierCreate, SellerCourierUpdate } from "../../src/api/types"; import { useToast } from "../hooks/use-toast"; import { useParams, useNavigate } from "@remix-run/react"; -import { useStoreOwnership } from "../hooks/useStoreOwnership"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; import { MetaFunction } from "@remix-run/node"; import { getPersianTextOfPrice, safeGoBack } from "~/utils/helpers"; @@ -37,8 +37,8 @@ export default function ShippingMethod() { const navigate = useNavigate(); const [savingCourierId, setSavingCourierId] = useState(null); - // Check store ownership - this will redirect if user doesn't own the store - const { isLoading: isOwnershipLoading } = useStoreOwnership(storeId); + // Shipping/courier config is owner-only — redirect staff back to the dashboard. + const { isLoading: isOwnershipLoading } = useRequireOwner(storeId); // Fetch all available couriers const {