Vitron-Front/app/routes/store.$storeId.financial-dashboard.cash-funds.tsx
Arda Samadi bb44ebf0e5 fix(seller-team): guard owner-only routes + robust useMyStores
Review fixes:
- useMyStores throws on a failed response instead of resolving to [] — a
  transient 401/blip no longer looks like "no stores" and bounces an owner off
  their own dashboard (query stays in error/retry, keeps prior data).
- Add useRequireOwner() guard and apply it to the owner-only routes
  (financial-dashboard + its cash-funds/reports/transactions sub-routes and
  shipping-method) so staff who hit those URLs directly are redirected to the
  dashboard. (Financial data was already backend-protected; this is the matching
  client-side gate.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:04:06 +03:30

815 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
ChevronLeft,
ChevronRight,
Loader,
Loader2,
Plus,
Trash2,
TriangleAlert,
X,
} from "lucide-react";
import HeaderWithSupport from "../components/HeaderWithSupport";
import {
Carousel,
CarouselApi,
CarouselContent,
CarouselItem,
} from "../components/ui/carousel";
import { useCallback, useState, useRef, useEffect } from "react";
import { useNavigate, useParams } from "@remix-run/react";
import { useCarousel } from "../hooks/useCarousel";
import { useRequireOwner } from "../hooks/useStoreOwnership";
import {
Drawer,
DrawerTitle,
DrawerContent,
DrawerHeader,
} from "../components/ui/drawer";
import {
useServiceCreateCashRequest,
useServiceGetSellerBalance,
useServiceGetBankAccounts,
useServiceRegisterBankAccount,
useServiceDeleteBankAccount,
} from "../requestHandler/use-wallet-hooks";
import {
formatAccountNumber,
formatPrice,
getPersianTextOfPrice,
safeGoBack,
} from "../utils/helpers";
import { Button } from "../components/ui/button";
import AppInput from "../components/AppInput";
import { Dialog, DialogContent, DialogOverlay } from "../components/ui/dialog";
import { Input } from "../components/ui/input";
import { MetaFunction } from "@remix-run/node";
import {
detectIranianBank,
validateIranianBankCard,
} from "~/utils/iranian-banks";
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);
};
const [currentIndex, setCurrentIndex] = useState(0);
const { api, setApi } = useCarousel({
onSelect: useCallback(
(carouselApi: CarouselApi) => {
if (!carouselApi) return;
setCurrentIndex(carouselApi.selectedScrollSnap());
},
[setCurrentIndex]
),
});
const [addCartDrawerOpen, setAddCartDrawerOpen] = useState(false);
const [deleteCardModalOpen, setDeleteCardModalOpen] = useState(false);
const [cardToDelete, setCardToDelete] = useState<string | undefined>();
const { data: balance, isLoading: isBalanceLoading } =
useServiceGetSellerBalance();
const { data: bankAccounts, isLoading: isBankAccountsLoading } =
useServiceGetBankAccounts();
const [cashDrawerIsOpen, setCashDrawerIsOpen] = useState(false);
return (
<div className="bg-WHITE pb-32">
<HeaderWithSupport title="برداشت وجه" onBack={handleBack} />
<div className="p-4 w-full flex flex-col gap-4 items-center">
<div className="flex w-full justify-between items-center">
<p className="text-B16 font-bold">کارتهای بانکی شما:</p>
<button
onClick={() => setAddCartDrawerOpen(true)}
className="bg-VITROWN_BLUE hover:bg-VITROWN_BLUE/90 transition-colors text-WHITE rounded-md p-3 shadow-lg flex items-center justify-center"
aria-label="افزودن کارت جدید"
>
<Plus size={24} />
</button>
</div>
{isBankAccountsLoading ? (
<div className="flex items-center justify-center h-[190px]">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
) : bankAccounts && bankAccounts.length > 0 ? (
<>
<Carousel
setApi={setApi}
opts={{
align: "start",
loop: true,
}}
className="w-full"
>
<CarouselContent className="w-full">
{bankAccounts.map((account, index) => (
<CarouselItem
key={account.id || index}
className="pl-0 w-full"
>
{(() => {
const detectedBank = detectIranianBank(
account.cardNumber || ""
);
return (
<div
className="w-full flex flex-col justify-between h-[190px] text-WHITE rounded-xl aspect-square overflow-hidden relative p-4"
style={{
background: detectedBank
? `linear-gradient(to right, ${detectedBank.color.from}, ${detectedBank.color.to})`
: "linear-gradient(to right, var(--gradient-gray-from), var(--gradient-gray-to))",
}}
>
{/* Delete button */}
<button
onClick={() => {
setCardToDelete(account.id);
setDeleteCardModalOpen(true);
}}
className="absolute top-3 left-3 bg-red-500 hover:bg-red-600 transition-colors rounded-full p-2 shadow-lg z-10"
aria-label="حذف کارت"
>
<Trash2 size={18} className="text-WHITE" />
</button>
<div className="flex w-full justify-between items-center">
<p className="text-B16 font-bold">
{detectedBank ? detectedBank.name : "بانک"}
</p>
{detectedBank ? (
<BankLogo bank={detectedBank} size={32} />
) : (
<div className="w-8 h-8 rounded bg-white/20 flex items-center justify-center">
<span className="text-xs">؟</span>
</div>
)}
</div>
<p className="text-B20 font-bold text-center">
{formatAccountNumber(account.cardNumber || "")}
</p>
<p className="text-B14 text-left font-bold">
{account.iban}
</p>
</div>
);
})()}
</CarouselItem>
))}
</CarouselContent>
</Carousel>
{bankAccounts.length > 1 && (
<div className="flex items-center gap-2">
<ChevronRight
onClick={() => {
api?.scrollPrev();
}}
className="font-bold text-GRAY ml-1 cursor-pointer"
size={20}
/>
{bankAccounts.map((_, index) => (
<button
key={index}
onClick={() => api?.scrollTo(index)}
className={`w-2 h-2 rounded-full transition-colors ${
index === currentIndex ? "bg-BLACK" : "bg-BLACK/50"
}`}
/>
))}
<ChevronLeft
onClick={() => {
api?.scrollNext();
}}
className="font-bold text-GRAY mr-1 cursor-pointer"
size={20}
/>
</div>
)}
</>
) : (
<div className="flex flex-col items-center justify-center h-[190px] text-GRAY">
<p className="text-B16">هیچ کارت بانکی ثبت نشده است</p>
<p className="text-R12 mt-2">
لطفا ابتدا کارت بانکی خود را اضافه کنید
</p>
</div>
)}
<div className="w-full gap-4 p-4 flex flex-col items-center justify-center bg-WHITE2 mt-10 rounded-[12px]">
<p className="text-B18 font-bold">موجودی کیف پول (تومان):</p>
{isBalanceLoading ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<p className="text-B24 font-bold">
{formatPrice(balance?.availableFunds || "0")}
</p>
)}
</div>
</div>
{/* Floating action buttons */}
<div className="md:max-w-md mx-auto fixed flex flex-row gap-2 w-full bottom-[50px] left-0 right-0 border-t py-2 px-2 z-50 bg-WHITE">
{/* Withdraw button */}
<Button
variant="dark"
size="lg"
className="w-full flex-1 font-bold bg-VITROWN_BLUE text-white "
onClick={() => setCashDrawerIsOpen(true)}
disabled={!bankAccounts || bankAccounts.length === 0}
>
برداشت پول
</Button>
</div>
{/* Modals and Drawers */}
<AddCartDrawer
isOpen={addCartDrawerOpen}
onOpenChange={setAddCartDrawerOpen}
/>
<DeleteCardModal
isOpen={deleteCardModalOpen}
onOpenChange={setDeleteCardModalOpen}
bankAccountId={cardToDelete}
/>
{bankAccounts && bankAccounts.length > 0 && (
<CashDrawer
isOpen={cashDrawerIsOpen}
onOpenChange={setCashDrawerIsOpen}
currentAccount={{
...bankAccounts[currentIndex],
cardNumber: bankAccounts[currentIndex]?.cardNumber || "",
}}
availableFunds={Number(balance?.availableFunds) || 0}
/>
)}
</div>
);
}
const DeleteCardModal = ({
isOpen,
onOpenChange,
bankAccountId,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
bankAccountId?: string;
}) => {
const { mutate: deleteBankAccount, isPending } =
useServiceDeleteBankAccount();
const onRemove = () => {
if (!bankAccountId) return;
deleteBankAccount(bankAccountId, {
onSuccess: () => {
onOpenChange(false);
},
onError: (error) => {
console.error("Failed to delete bank account:", error);
},
});
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogOverlay className="bg-BLACK/50" />
<DialogContent className="p-4 bg-WHITE rounded-[15px] w-[calc(100%-40px)]">
<div className="flex flex-col gap-10">
<div className={"flex flex-col gap-3 items-center w-full"}>
<Trash2 className="text-RED text-6" />
<p className="text-B12 font-bold mt-1">حذف کارت</p>
<p className="text-R12">از حذف این کارت مطمئن هستید؟</p>
</div>
<div className="flex flex-row gap-6 w-full">
<Button
disabled={isPending}
className="w-[100%]"
size={"lg"}
variant="dark"
onClick={() => onRemove()}
>
{isPending ? (
<>
<div className="relative w-5 h-5 mr-2">
<div className="absolute top-0 left-0 w-full h-full border-2 border-t-RED border-r-transparent border-b-transparent border-l-transparent rounded-full animate-spin"></div>
<div
className="absolute top-0.5 left-0.5 w-4 h-4 border-2 border-t-transparent border-r-RED border-b-transparent border-l-transparent rounded-full animate-spin"
style={{
animationDirection: "reverse",
animationDuration: "0.7s",
}}
></div>
</div>
درحال حذف...
</>
) : (
"حذف کارت"
)}
</Button>
<Button
className="w-[100%]"
size={"lg"}
variant="primary"
onClick={() => onOpenChange(false)}
>
انصراف
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};
const AddCartDrawer = ({
isOpen,
onOpenChange,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}) => {
const [cardPart1, setCardPart1] = useState("");
const [cardPart2, setCardPart2] = useState("");
const [cardPart3, setCardPart3] = useState("");
const [cardPart4, setCardPart4] = useState("");
const [shabaNumber, setShabaNumber] = useState("");
const [cardError, setCardError] = useState("");
const { mutate: registerBankAccount, isPending: isSubmitting } =
useServiceRegisterBankAccount();
const cardRef1 = useRef<HTMLInputElement>(null);
const cardRef2 = useRef<HTMLInputElement>(null);
const cardRef3 = useRef<HTMLInputElement>(null);
const cardRef4 = useRef<HTMLInputElement>(null);
// Detect bank from current card input
const fullCardNumber = cardPart1 + cardPart2 + cardPart3 + cardPart4;
const detectedBank = detectIranianBank(fullCardNumber);
const handleCardInputChange = (
value: string,
part: number,
setter: (value: string) => void
) => {
// فقط عدد قبول کند
const numericValue = value.replace(/\D/g, "");
if (numericValue.length <= 4) {
setter(numericValue);
// اگر 4 رقم پر شد، به بعدی برو
if (numericValue.length === 4) {
if (part === 1) cardRef2.current?.focus();
else if (part === 2) cardRef3.current?.focus();
else if (part === 3) cardRef4.current?.focus();
}
}
};
const handleSubmit = () => {
const formattedIban = `IR${shabaNumber}`;
// Validate Iranian bank card
if (!validateIranianBankCard(fullCardNumber)) {
setCardError("شماره کارت وارد شده متعلق به بانک‌های ایران نیست");
return;
}
registerBankAccount(
{
iban: formattedIban,
cardNumber: fullCardNumber,
},
{
onSuccess: () => {
// Reset form
setCardPart1("");
setCardPart2("");
setCardPart3("");
setCardPart4("");
setShabaNumber("");
setCardError("");
onOpenChange(false);
},
onError: (error) => {
console.error("Failed to register bank account:", error);
setCardError("خطا در ثبت کارت بانکی");
},
}
);
};
const isFormValid =
cardPart1.length === 4 &&
cardPart2.length === 4 &&
cardPart3.length === 4 &&
cardPart4.length === 4 &&
shabaNumber.length === 24 &&
validateIranianBankCard(fullCardNumber);
return (
<Drawer open={isOpen} onOpenChange={onOpenChange}>
<DrawerContent>
<DrawerHeader className="flex items-center relative justify-center">
<X
size={24}
className="text-BLACK2 absolute left-4"
onClick={() => onOpenChange(false)}
/>
<DrawerTitle className="text-center text-B12 font-bold">
افزودن کارت جدید
</DrawerTitle>
</DrawerHeader>
<div className="flex flex-col gap-4 p-4 pb-6">
<div className="w-full flex flex-col gap-2">
<p className="text-R14">شماره کارت را وارد کنید:</p>
<div className="flex items-center" dir="ltr">
<AppInput
inputTitle=""
inputValue={cardPart1}
inputPlaceholder="----"
inputDir="ltr"
className="text-center w-20"
inputRef={cardRef1}
inputOnChange={(e) => {
handleCardInputChange(e.target.value, 1, setCardPart1);
setCardError("");
}}
/>
<AppInput
inputTitle=""
inputValue={cardPart2}
inputPlaceholder="----"
inputDir="ltr"
className="text-center w-20"
inputRef={cardRef2}
inputOnChange={(e) => {
handleCardInputChange(e.target.value, 2, setCardPart2);
setCardError("");
}}
/>
<AppInput
inputTitle=""
inputValue={cardPart3}
inputPlaceholder="----"
inputDir="ltr"
className="text-center w-20"
inputRef={cardRef3}
inputOnChange={(e) => {
handleCardInputChange(e.target.value, 3, setCardPart3);
setCardError("");
}}
/>
<AppInput
inputTitle=""
inputValue={cardPart4}
inputPlaceholder="----"
inputDir="ltr"
className="text-center w-20"
inputRef={cardRef4}
inputOnChange={(e) => {
handleCardInputChange(e.target.value, 4, setCardPart4);
setCardError("");
}}
/>
</div>
{/* Show detected bank */}
{detectedBank && fullCardNumber.length >= 6 && (
<div className="flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-lg animate-in fade-in slide-in-from-top-2 duration-300">
<BankLogo bank={detectedBank} size={24} />
<div className="flex flex-col">
<p className="text-R12 font-bold text-green-800">
بانک {detectedBank.name}
</p>
<p className="text-R10 text-green-600">کارت شناسایی شد</p>
</div>
</div>
)}
{/* Show error if card is complete but invalid */}
{fullCardNumber.length === 16 && !detectedBank && (
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg animate-in fade-in slide-in-from-top-2 duration-300">
<TriangleAlert size={20} className="text-red-600" />
<p className="text-R12 text-red-700">
شماره کارت متعلق به بانکهای ایران نیست
</p>
</div>
)}
{/* Show custom error */}
{cardError && (
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
<TriangleAlert size={20} className="text-red-600" />
<p className="text-R12 text-red-700">{cardError}</p>
</div>
)}
</div>
<div className="w-full flex flex-col gap-2">
<p className="text-R14">شماره شبا حساب خود را وارد کنید:</p>
<div className="relative w-full">
<Input
type="text"
placeholder="شماره شبا"
value={shabaNumber}
onChange={(e) => setShabaNumber(e.target.value)}
dir={!shabaNumber ? "rtl" : "ltr"}
className="pl-14"
/>
<div className="absolute top-4 left-3 text-GRAY text-sm font-medium">
IR
</div>
</div>
</div>
<Button
variant="dark"
size="lg"
onClick={handleSubmit}
disabled={!isFormValid || isSubmitting}
className="w-full font-bold mt-4"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin mr-2" />
در حال ثبت...
</>
) : (
"ثبت و در انتظار تایید"
)}
</Button>
</div>
</DrawerContent>
</Drawer>
);
};
const CashDrawer = ({
isOpen,
onOpenChange,
currentAccount,
availableFunds,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
currentAccount: {
id?: string;
iban: string;
cardNumber: string;
isVerified: boolean;
createdAt: string;
};
availableFunds: number;
}) => {
const [amount, setAmount] = useState<number | null>(null);
const [withdrawAll, setWithdrawAll] = useState(false);
const { mutate: createCashRequest, isPending: submitLoading } =
useServiceCreateCashRequest();
const [successModalIsOpen, setSuccessModalIsOpen] = useState(false);
const [errorModalIsOpen, setErrorModalIsOpen] = useState(false);
// Reset state when drawer closes
useEffect(() => {
if (!isOpen) {
setAmount(null);
setWithdrawAll(false);
}
}, [isOpen]);
return (
<Drawer open={isOpen} onOpenChange={onOpenChange}>
<DrawerContent>
<DrawerHeader className="flex items-center relative justify-center">
<X
size={24}
className="text-BLACK2 absolute left-4"
onClick={() => onOpenChange(false)}
/>
<DrawerTitle className="text-center text-B10 font-bold">
مبلغ برداشت را تعیین کنید:
</DrawerTitle>
</DrawerHeader>
<div className="flex flex-col w-full gap-6 p-4">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<AppInput
inputTitle="مبلغ"
type="number"
inputPlaceholder="مبلغ برداشت را وارد کنید"
inputValue={
withdrawAll
? availableFunds.toString()
: amount?.toString() || ""
}
inputDir={amount === null && !withdrawAll ? "rtl" : "ltr"}
disabled={withdrawAll}
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAmount(parseInt(e.target.value) || null);
setWithdrawAll(false);
}}
/>
{/* Show Persian text of entered amount */}
{amount !== null && amount > 0 && (
<div className="px-2 py-2 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-R12 text-blue-800">
{getPersianTextOfPrice(amount.toString())}
</p>
</div>
)}
{/* Show maximum withdrawal amount */}
<div className="flex items-center justify-between px-2">
<p className="text-R12 text-GRAY">حداکثر قابل برداشت:</p>
<p className="text-R14 font-bold text-GREEN">
{formatPrice(availableFunds.toString())} تومان
</p>
</div>
</div>
{/* Toggle switch for withdraw all */}
<div className="flex items-center justify-between gap-3 pr-1">
<label
htmlFor="withdrawAll"
className="text-R14 cursor-pointer select-none"
>
برداشت کل موجودی ({formatPrice(availableFunds.toString())}{" "}
تومان)
</label>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
id="withdrawAll"
checked={withdrawAll}
onChange={(e) => {
setWithdrawAll(e.target.checked);
if (e.target.checked) {
setAmount(availableFunds);
} else {
setAmount(null);
}
}}
className="sr-only peer"
aria-label="برداشت کل موجودی"
/>
<div className="w-11 h-6 bg-WHITE2 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-WHITE after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-WHITE after:border-GRAY3 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-VITROWN_BLUE border"></div>
</label>
</div>
</div>
<div className="flex flex-col gap-2 items-center justify-center ">
<TriangleAlert size={20} />
<p className="text-B14 font-bold text-center">
مبلغ تعیین شده بعد از بررسی، به حساب{" "}
<span className="font-bold">
{formatAccountNumber(currentAccount.cardNumber)
.split("-")
.reverse()
.join("-")}
</span>{" "}
با شماره شبا{" "}
<span className="font-bold">{currentAccount.iban}</span> منتقل
خواهد شد
</p>
</div>
<Button
variant="dark"
size="lg"
onClick={() => {
const finalAmount = withdrawAll ? availableFunds : amount;
createCashRequest(
{
amount: finalAmount?.toString() || "0",
bank_account: currentAccount.id || "",
},
{
onError: () => {
onOpenChange(false);
setErrorModalIsOpen(true);
},
onSuccess: () => {
onOpenChange(false);
setSuccessModalIsOpen(true);
setAmount(null);
setWithdrawAll(false);
},
}
);
}}
disabled={
(amount === null && !withdrawAll) ||
amount === 0 ||
submitLoading ||
(amount !== null && amount > availableFunds)
}
className="w-full font-bold bg-VITROWN_BLUE text-white"
>
{submitLoading && <Loader2 className="w-4 h-4 animate-spin mr-2" />}
ثبت درخواست
</Button>
</div>
</DrawerContent>
<SuccessModal
isOpen={successModalIsOpen}
onOpenChange={setSuccessModalIsOpen}
/>
<ErrorModal
isOpen={errorModalIsOpen}
onOpenChange={setErrorModalIsOpen}
/>
</Drawer>
);
};
const SuccessModal = ({
isOpen,
onOpenChange,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}) => {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogOverlay className="bg-BLACK/50" />
<DialogContent className="p-6 bg-WHITE rounded-[15px] w-[calc(100%-40px)] max-w-sm">
<div className="flex flex-col items-center justify-center gap-4 py-4">
<Loader />
<p className="text-B14 font-bold text-center">در انتظار تایید مبلغ</p>
<p className="text-R12 text-center">
پس از تایید، مبلغ مورد نظر به حساب شما واریز میشود. این فرآیند کمتر
از یک ساعت زمان میبرد.
</p>
<Button
onClick={() => onOpenChange(false)}
variant="dark"
size="lg"
className="w-full font-bold"
>
فهمیدم
</Button>
</div>
</DialogContent>
</Dialog>
);
};
const ErrorModal = ({
isOpen,
onOpenChange,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}) => {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogOverlay className="bg-BLACK/50" />
<DialogContent className="p-6 bg-WHITE rounded-[15px] w-[calc(100%-40px)] max-w-sm">
<div className="flex flex-col items-center justify-center gap-4 py-4">
<TriangleAlert size={20} />
<p className="text-B14 font-bold text-center">
درخواست برداشت مبلغ با خطا مواجه شد
</p>
<p className="text-R12 text-center">
لطفا مجددا درخواست برداشت را انجام دهید. یا با پشتیبانی تماس بگیرید.
</p>
<Button
onClick={() => onOpenChange(false)}
variant="dark"
size="lg"
className="w-full font-bold"
>
فهمیدم
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export const meta: MetaFunction = () => {
return [
{ title: "کیف پول - ویترون" },
{
name: "description",
content:
"مدیریت و مشاهده تمام کیف پول در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در کیف پول.",
},
{
name: "keywords",
content: "کیف پول، مدیریت کیف پول، فروشگاه، مشتری، ویترون",
},
{ name: "robots", content: "noindex, follow" },
{ property: "og:title", content: "کیف پول - ویترون" },
{
property: "og:description",
content:
"مدیریت و مشاهده تمام کیف پول در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در کیف پول.",
},
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary" },
{ name: "twitter:title", content: "کیف پول - ویترون" },
{
name: "twitter:description",
content:
"مدیریت و مشاهده تمام کیف پول در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در کیف پول.",
},
];
};