tr]:last:border-b-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ );
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ | [role=checkbox]]:translate-y-[2px]",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ );
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+};
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
new file mode 100644
index 00000000..469a958d
--- /dev/null
+++ b/src/components/ui/tabs.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import * as React from "react";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+
+import { cn } from "@/lib/utils";
+
+function Tabs({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 00000000..0735a8ca
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ );
+}
+
+export { Textarea };
diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx
new file mode 100644
index 00000000..73b05cfa
--- /dev/null
+++ b/src/components/ui/toast.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import * as React from "react";
+import * as ToastPrimitives from "@radix-ui/react-toast";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+import { X } from "lucide-react";
+
+const ToastProvider = ToastPrimitives.Provider;
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ );
+});
+Toast.displayName = ToastPrimitives.Root.displayName;
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastAction.displayName = ToastPrimitives.Action.displayName;
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+ToastClose.displayName = ToastPrimitives.Close.displayName;
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastTitle.displayName = ToastPrimitives.Title.displayName;
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastDescription.displayName = ToastPrimitives.Description.displayName;
+
+type ToastProps = React.ComponentPropsWithoutRef;
+
+type ToastActionElement = React.ReactElement;
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+};
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 00000000..bf4a342a
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import * as React from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cn } from "@/lib/utils";
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ );
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/src/components/utils/ui/Create.tsx b/src/components/utils/ui/Create.tsx
new file mode 100644
index 00000000..40be6065
--- /dev/null
+++ b/src/components/utils/ui/Create.tsx
@@ -0,0 +1,19 @@
+import Link from "next/link";
+import { Button } from "../../ui/button";
+
+interface CreateButtonProps {
+ label: string;
+ url: string;
+ className?: string;
+ id?: string;
+}
+
+const CreateButton = ({ label, url, className, id }: CreateButtonProps) => {
+ return (
+
+
+
+ );
+};
+
+export default CreateButton;
diff --git a/src/components/utils/ui/Divider.tsx b/src/components/utils/ui/Divider.tsx
new file mode 100644
index 00000000..65ad380b
--- /dev/null
+++ b/src/components/utils/ui/Divider.tsx
@@ -0,0 +1,17 @@
+interface DividerProps {
+ type: "horizontal" | "vertical";
+}
+
+const Divider = ({ type }: DividerProps) => {
+ return (
+
+ );
+};
+
+export default Divider;
diff --git a/src/components/utils/ui/Loader.tsx b/src/components/utils/ui/Loader.tsx
new file mode 100644
index 00000000..ce25c043
--- /dev/null
+++ b/src/components/utils/ui/Loader.tsx
@@ -0,0 +1,28 @@
+import { TailSpin } from "react-loader-spinner";
+
+interface LoaderProps {
+ isLoading: boolean;
+}
+
+const Loader: React.FC = ({ isLoading }) => {
+ if (isLoading) {
+ return (
+
+ );
+ }
+ return null;
+};
+
+export default Loader;
diff --git a/src/components/utils/ui/NoData.tsx b/src/components/utils/ui/NoData.tsx
new file mode 100644
index 00000000..1e90b698
--- /dev/null
+++ b/src/components/utils/ui/NoData.tsx
@@ -0,0 +1,19 @@
+import { ArchiveX } from "lucide-react";
+
+interface NoDataProps {
+ isCard?: boolean;
+}
+
+const NoData = ({ isCard }: NoDataProps) => {
+ return (
+
+ );
+};
+
+export default NoData;
diff --git a/src/components/utils/ui/SelectSearch.tsx b/src/components/utils/ui/SelectSearch.tsx
new file mode 100644
index 00000000..678c2eab
--- /dev/null
+++ b/src/components/utils/ui/SelectSearch.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import React, { useState } from "react";
+import {
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../ui/form";
+import TooltipInfo from "./Tooltip";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@radix-ui/react-popover";
+import { Button } from "../../ui/button";
+import { ChevronsUpDown } from "lucide-react";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "../../ui/command";
+import { useInitializeEscrow } from "@/components/modules/escrow/hooks/initialize-escrow.hook";
+
+interface SelectFieldProps {
+ control: any;
+ name: string;
+ label: string;
+ tooltipContent: string;
+ options: { value: string | undefined; label: string }[];
+ className?: string;
+ required?: boolean;
+}
+
+const SelectField: React.FC = ({
+ control,
+ name,
+ label,
+ tooltipContent,
+ options,
+ className,
+ required,
+}) => {
+ const { handleFieldChange } = useInitializeEscrow();
+ const [open, setOpen] = useState(false);
+ const [selected, setSelected] = useState(options[0]);
+
+ const handleSelect = (option: {
+ value: string | undefined;
+ label: string;
+ }) => {
+ setSelected(option);
+ handleFieldChange(name, option.value);
+ setOpen(false);
+ };
+
+ return (
+ {
+ return (
+
+ {label && (
+
+ {label}{" "}
+ {required && *}
+ {tooltipContent && }
+
+ )}
+
+
+
+
+
+
+
+
+
+ No options found.
+
+ {options.map((option) => (
+ {
+ setSelected(option);
+ field.onChange(option.value);
+ handleSelect(option);
+ }}
+ >
+ {option.label}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+ }}
+ />
+ );
+};
+
+export default SelectField;
diff --git a/src/components/utils/ui/Tooltip.tsx b/src/components/utils/ui/Tooltip.tsx
new file mode 100644
index 00000000..394fbff9
--- /dev/null
+++ b/src/components/utils/ui/Tooltip.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { Info } from "lucide-react";
+
+interface TooltipInfoProps {
+ content: string;
+}
+
+const TooltipInfo = ({ content }: TooltipInfoProps) => {
+ return (
+
+
+
+
+
+ More information
+
+
+
+ {content}
+
+
+
+ );
+};
+
+export default TooltipInfo;
diff --git a/src/components/utils/wallet/getStellarAddress.ts b/src/components/utils/wallet/getStellarAddress.ts
new file mode 100644
index 00000000..16547069
--- /dev/null
+++ b/src/components/utils/wallet/getStellarAddress.ts
@@ -0,0 +1,14 @@
+export const getStellarAddress = (): string | null => {
+ if (typeof window === "undefined") return null;
+
+ const item = localStorage.getItem("address-wallet");
+ if (!item) return null;
+
+ try {
+ const parsed = JSON.parse(item);
+ return parsed.state?.address ?? null;
+ } catch (err) {
+ console.error("โ Error parsing wallet address:", err);
+ return null;
+ }
+};
diff --git a/src/core/config/axios/http.ts b/src/core/config/axios/http.ts
new file mode 100644
index 00000000..93842768
--- /dev/null
+++ b/src/core/config/axios/http.ts
@@ -0,0 +1,12 @@
+import axios from "axios";
+
+const http = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_API_URL || "",
+ timeout: 60000, // 1 minute
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${process.env.NEXT_PUBLIC_API_KEY}`,
+ },
+});
+
+export default http;
diff --git a/src/core/config/firebase/firebase.ts b/src/core/config/firebase/firebase.ts
new file mode 100644
index 00000000..0b88fa99
--- /dev/null
+++ b/src/core/config/firebase/firebase.ts
@@ -0,0 +1,4 @@
+import { getFirestore } from "firebase/firestore";
+import { firebaseApp } from "../../../../firebase";
+
+export const db = getFirestore(firebaseApp);
diff --git a/src/core/store/data/@types/authentication.entity.ts b/src/core/store/data/@types/authentication.entity.ts
new file mode 100644
index 00000000..2f5e8877
--- /dev/null
+++ b/src/core/store/data/@types/authentication.entity.ts
@@ -0,0 +1,13 @@
+import { User, UserPayload } from "@/@types/user.entity";
+
+export interface AuthenticationGlobalStore {
+ address: string;
+ name: string;
+ loggedUser: User | null;
+ users: User[];
+
+ connectWalletStore: (address: string, name: string) => void;
+ disconnectWalletStore: () => void;
+ updateUser: (address: string, payload: UserPayload) => void;
+ getAllUsers: () => void;
+}
diff --git a/src/core/store/data/@types/escrows.entity.ts b/src/core/store/data/@types/escrows.entity.ts
new file mode 100644
index 00000000..c6cec5c1
--- /dev/null
+++ b/src/core/store/data/@types/escrows.entity.ts
@@ -0,0 +1,28 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { Escrow, EscrowPayload } from "@/@types/escrow.entity";
+
+export interface EscrowGlobalStore {
+ escrows: Escrow[];
+ totalEscrows: number;
+ loadingEscrows: boolean;
+ selectedEscrow: Escrow | null;
+ escrowsToDelete: string[];
+ userRolesInEscrow: string[];
+ recentEscrow: Escrow | undefined;
+
+ setEscrows: (escrows: Escrow[]) => void;
+ fetchAllEscrows: (params: { address: string; type: string }) => void;
+ addEscrow: (
+ payload: EscrowPayload,
+ address: string,
+ contractId: string,
+ ) => Promise;
+ updateEscrow: (params: {
+ escrowId: string;
+ payload: EscrowPayload;
+ }) => Promise;
+ setUserRolesInEscrow: (roles: string[]) => void;
+ setRecentEscrow: (escrow: Escrow | undefined) => void;
+ setSelectedEscrow: (escrow: Escrow | undefined) => void;
+}
diff --git a/src/core/store/data/@types/trustlines.entity.ts b/src/core/store/data/@types/trustlines.entity.ts
new file mode 100644
index 00000000..40a7f7c5
--- /dev/null
+++ b/src/core/store/data/@types/trustlines.entity.ts
@@ -0,0 +1,7 @@
+import { Trustline } from "@/@types/trustline.entity";
+
+export interface TrustlineGlobalStore {
+ trustlines: Trustline[];
+
+ getAllTrustlines: () => void;
+}
diff --git a/src/core/store/data/index.ts b/src/core/store/data/index.ts
new file mode 100644
index 00000000..f8f99888
--- /dev/null
+++ b/src/core/store/data/index.ts
@@ -0,0 +1,60 @@
+import { devtools, DevtoolsOptions, persist } from "zustand/middleware";
+import { EscrowGlobalStore } from "./@types/escrows.entity";
+import { create } from "zustand";
+import { useGlobalEscrowsSlice } from "./slices/escrows.slice";
+import { useGlobalAuthenticationSlice } from "./slices/authentication.slice";
+import { AuthenticationGlobalStore } from "./@types/authentication.entity";
+import { TrustlineGlobalStore } from "./@types/trustlines.entity";
+import { useGlobalTrustlinesSlice } from "./slices/trustlines.slice";
+
+type GlobalState = EscrowGlobalStore & TrustlineGlobalStore;
+type AuthState = AuthenticationGlobalStore;
+
+const devtoolsOptions: DevtoolsOptions = {
+ name: "Global State",
+ serialize: {
+ options: {
+ undefined: true,
+ function: false,
+ symbol: false,
+ error: true,
+ date: true,
+ regexp: true,
+ bigint: true,
+ map: true,
+ set: true,
+ depth: 10,
+ maxSize: 50000,
+ },
+ },
+ enabled: process.env.NODE_ENV === "development",
+ anonymousActionType: "Unknown",
+ stateSanitizer: (state: GlobalState) => {
+ return {
+ ...state,
+ notificationsApi: "",
+ contextHolder: "",
+ };
+ },
+};
+
+export const useGlobalBoundedStore = create()(
+ devtools(
+ (...a) => ({
+ ...useGlobalEscrowsSlice(...a),
+ ...useGlobalTrustlinesSlice(...a),
+ }),
+ devtoolsOptions,
+ ),
+);
+
+export const useGlobalAuthenticationStore = create()(
+ persist(
+ (...b) => ({
+ ...useGlobalAuthenticationSlice(...b),
+ }),
+ {
+ name: "address-wallet",
+ },
+ ),
+);
diff --git a/src/core/store/data/slices/authentication.slice.ts b/src/core/store/data/slices/authentication.slice.ts
new file mode 100644
index 00000000..e41ecf90
--- /dev/null
+++ b/src/core/store/data/slices/authentication.slice.ts
@@ -0,0 +1,85 @@
+import { StateCreator } from "zustand";
+import { AuthenticationGlobalStore } from "../@types/authentication.entity";
+import {
+ addUser,
+ getAllUsers,
+ getUser,
+ updateUser,
+} from "@/components/modules/auth/server/authentication.firebase";
+import { UserPayload } from "@/@types/user.entity";
+
+const AUTHENTICATION_ACTIONS = {
+ CONNECT_WALLET: "authentication/connect",
+ DISCONNECT_WALLET: "authentication/disconnect",
+ UPDATE_USER: "authentication/updateUser",
+ REMOVE_API_KEY: "authentication/removeApiKey",
+ SET_USERS: "authentication/setUsers",
+} as const;
+
+export const useGlobalAuthenticationSlice: StateCreator<
+ AuthenticationGlobalStore,
+ [["zustand/devtools", never]],
+ [],
+ AuthenticationGlobalStore
+> = (set) => {
+ return {
+ // Stores
+ address: "",
+ name: "",
+ loggedUser: null,
+ users: [],
+
+ // Modifiers
+ connectWalletStore: async (address: string, name: string) => {
+ const { success, data } = await getUser({ address });
+
+ if (!success) {
+ const { success: registrationSuccess, data: userData } = await addUser({
+ address,
+ });
+
+ if (registrationSuccess) {
+ set(
+ { address, name, loggedUser: userData },
+ false,
+ AUTHENTICATION_ACTIONS.CONNECT_WALLET,
+ );
+ }
+ } else {
+ set(
+ { address, name, loggedUser: data },
+ false,
+ AUTHENTICATION_ACTIONS.CONNECT_WALLET,
+ );
+ }
+ },
+
+ disconnectWalletStore: () =>
+ set(
+ { address: "", name: "", loggedUser: null },
+ false,
+ AUTHENTICATION_ACTIONS.DISCONNECT_WALLET,
+ ),
+
+ updateUser: async (address: string, payload: UserPayload) => {
+ const { success, data } = await updateUser({
+ address,
+ payload,
+ });
+
+ if (success) {
+ set({ loggedUser: data }, false, AUTHENTICATION_ACTIONS.UPDATE_USER);
+ }
+ },
+
+ getAllUsers: async () => {
+ const { success, message, data } = await getAllUsers();
+
+ if (success) {
+ set({ users: data }, false, AUTHENTICATION_ACTIONS.SET_USERS);
+ } else {
+ console.error(message);
+ }
+ },
+ };
+};
diff --git a/src/core/store/data/slices/escrows.slice.ts b/src/core/store/data/slices/escrows.slice.ts
new file mode 100644
index 00000000..d8f70257
--- /dev/null
+++ b/src/core/store/data/slices/escrows.slice.ts
@@ -0,0 +1,124 @@
+import type { StateCreator } from "zustand";
+import type { EscrowGlobalStore } from "../@types/escrows.entity";
+import type { Escrow } from "@/@types/escrow.entity";
+import {
+ fetchAllEscrows,
+ addNewEscrow,
+ updateExistingEscrow,
+} from "@/components/modules/escrow/services/escrow.service";
+
+const ESCROW_ACTIONS = {
+ SET_ESCROWS: "escrows/set",
+ SET_SELECTED_ESCROW: "escrows/setSelected",
+ FETCH_ALL_ESCROWS: "escrows/fetchAll",
+ ADD_ESCROW: "escrows/add",
+ UPDATE_ESCROW: "escrows/update",
+ DELETE_PRODUCT: "escrows/delete",
+ SET_ESCROW_TO_DELETE: "escrows/setToDelete",
+ SET_LOADING_ESCROWS: "escrows/setLoading",
+ SET_USER_ROLE: "escrows/setUserRole",
+} as const;
+
+export const ESCROW_SLICE_NAME = "escrowSlice" as const;
+
+export const useGlobalEscrowsSlice: StateCreator<
+ EscrowGlobalStore,
+ [["zustand/devtools", never]],
+ [],
+ EscrowGlobalStore
+> = (set) => {
+ return {
+ // State
+ escrows: [],
+ totalEscrows: 0,
+ loadingEscrows: false,
+ escrowsToDelete: [],
+ selectedEscrow: null,
+ userRolesInEscrow: [],
+ recentEscrow: undefined,
+ approverFunds: "",
+ serviceProviderFunds: "",
+
+ // Actions
+ setEscrows: (escrows: Escrow[]) =>
+ set({ escrows }, false, ESCROW_ACTIONS.SET_ESCROWS),
+
+ setSelectedEscrow: (escrow: Escrow | undefined) =>
+ set(
+ { selectedEscrow: escrow },
+ false,
+ ESCROW_ACTIONS.SET_SELECTED_ESCROW,
+ ),
+
+ fetchAllEscrows: async ({ address, type = "approver" }) => {
+ set({ loadingEscrows: true }, false, ESCROW_ACTIONS.SET_LOADING_ESCROWS);
+ try {
+ const escrows = await fetchAllEscrows({ address, type });
+ set(
+ { escrows, loadingEscrows: false },
+ false,
+ ESCROW_ACTIONS.SET_ESCROWS,
+ );
+ } catch (error) {
+ set(
+ { loadingEscrows: false },
+ false,
+ ESCROW_ACTIONS.SET_LOADING_ESCROWS,
+ );
+ throw error;
+ }
+ },
+
+ addEscrow: async (payload, address, contractId) => {
+ const newEscrow = await addNewEscrow(payload, address, contractId);
+ if (newEscrow) {
+ set(
+ (state) => ({
+ escrows: [newEscrow, ...state.escrows],
+ }),
+ false,
+ ESCROW_ACTIONS.ADD_ESCROW,
+ );
+ }
+ return newEscrow;
+ },
+
+ updateEscrow: async ({ escrowId, payload }) => {
+ set({ loadingEscrows: true }, false, ESCROW_ACTIONS.SET_LOADING_ESCROWS);
+ try {
+ const updatedEscrow = await updateExistingEscrow({ escrowId, payload });
+ if (updatedEscrow) {
+ set(
+ (state) => ({
+ escrows: state.escrows.map((escrow) =>
+ escrow.id === escrowId ? updatedEscrow : escrow,
+ ),
+ }),
+ false,
+ ESCROW_ACTIONS.UPDATE_ESCROW,
+ );
+ }
+ set(
+ { loadingEscrows: false },
+ false,
+ ESCROW_ACTIONS.SET_LOADING_ESCROWS,
+ );
+ return updatedEscrow;
+ } catch (error) {
+ set(
+ { loadingEscrows: false },
+ false,
+ ESCROW_ACTIONS.SET_LOADING_ESCROWS,
+ );
+ throw error;
+ }
+ },
+
+ setUserRolesInEscrow: (role) =>
+ set({ userRolesInEscrow: role }, false, ESCROW_ACTIONS.SET_USER_ROLE),
+
+ setRecentEscrow: (escrow: Escrow | undefined) => {
+ set({ recentEscrow: escrow });
+ },
+ };
+};
diff --git a/src/core/store/data/slices/trustlines.slice.ts b/src/core/store/data/slices/trustlines.slice.ts
new file mode 100644
index 00000000..707f366a
--- /dev/null
+++ b/src/core/store/data/slices/trustlines.slice.ts
@@ -0,0 +1,30 @@
+import { StateCreator } from "zustand";
+import { TrustlineGlobalStore } from "../@types/trustlines.entity";
+import { getAllTrustlines } from "@/components/modules/token/server/trustline.firebase";
+
+const TRUSTLINE_ACTIONS = {
+ SET_TRUSTLINES: "trustlines/setTrustlines",
+} as const;
+
+export const useGlobalTrustlinesSlice: StateCreator<
+ TrustlineGlobalStore,
+ [["zustand/devtools", never]],
+ [],
+ TrustlineGlobalStore
+> = (set) => {
+ return {
+ // Stores
+ trustlines: [],
+
+ // Modifiers
+ getAllTrustlines: async () => {
+ const { success, message, data } = await getAllTrustlines();
+
+ if (success) {
+ set({ trustlines: data }, false, TRUSTLINE_ACTIONS.SET_TRUSTLINES);
+ } else {
+ console.error(message);
+ }
+ },
+ };
+};
diff --git a/src/core/store/ui/@types/loader.entity.ts b/src/core/store/ui/@types/loader.entity.ts
new file mode 100644
index 00000000..4681e491
--- /dev/null
+++ b/src/core/store/ui/@types/loader.entity.ts
@@ -0,0 +1,4 @@
+export interface LoaderGlobalUIStore {
+ isLoading: boolean;
+ setIsLoading: (isLoading: boolean) => void;
+}
diff --git a/src/core/store/ui/@types/steps.entity.ts b/src/core/store/ui/@types/steps.entity.ts
new file mode 100644
index 00000000..45932e49
--- /dev/null
+++ b/src/core/store/ui/@types/steps.entity.ts
@@ -0,0 +1,14 @@
+export interface StepsGlobalUIStore {
+ currentStep: number;
+ totalSteps: number;
+ completedSteps: Set;
+ setCurrentStep: (step: number) => void;
+ setTotalSteps: (total: number) => void;
+ toggleStep: (step: number) => void;
+ isStepCompleted: (step: number) => boolean;
+ nextStep: () => void;
+ previousStep: () => void;
+ isFirstStep: () => boolean;
+ isLastStep: () => boolean;
+ resetSteps: () => void;
+}
diff --git a/src/core/store/ui/@types/theme.entity.ts b/src/core/store/ui/@types/theme.entity.ts
new file mode 100644
index 00000000..721bda34
--- /dev/null
+++ b/src/core/store/ui/@types/theme.entity.ts
@@ -0,0 +1,4 @@
+export interface ThemeGlobalUIStore {
+ theme: "light" | "dark";
+ toggleTheme: (newTheme?: "light" | "dark") => void;
+}
diff --git a/src/core/store/ui/@types/tutorial.entity.ts b/src/core/store/ui/@types/tutorial.entity.ts
new file mode 100644
index 00000000..d15b2b89
--- /dev/null
+++ b/src/core/store/ui/@types/tutorial.entity.ts
@@ -0,0 +1,4 @@
+export interface TutorialGlobalUIStore {
+ run: boolean;
+ setRun: (run: boolean) => void;
+}
diff --git a/src/core/store/ui/index.ts b/src/core/store/ui/index.ts
new file mode 100644
index 00000000..050ec263
--- /dev/null
+++ b/src/core/store/ui/index.ts
@@ -0,0 +1,66 @@
+import {
+ createJSONStorage,
+ devtools,
+ DevtoolsOptions,
+ persist,
+} from "zustand/middleware";
+import { create } from "zustand";
+import { ThemeGlobalUIStore } from "./@types/theme.entity";
+import { useThemeSlice } from "./slices/theme.slice";
+import { LoaderGlobalUIStore } from "./@types/loader.entity";
+import { useLoaderSlice } from "./slices/loader.slice";
+import { StepsGlobalUIStore } from "./@types/steps.entity";
+import { useStepsSlice } from "./slices/steps.slice";
+import { useTutorialSlice } from "./slices/tutorial.slice";
+import { TutorialGlobalUIStore } from "./@types/tutorial.entity";
+
+type GlobalUIState = ThemeGlobalUIStore &
+ LoaderGlobalUIStore &
+ StepsGlobalUIStore &
+ TutorialGlobalUIStore;
+
+const devtoolsOptions: DevtoolsOptions = {
+ name: "Global UI State",
+ serialize: {
+ options: {
+ undefined: true,
+ function: false,
+ symbol: false,
+ error: true,
+ date: true,
+ regexp: true,
+ bigint: true,
+ map: true,
+ set: true,
+ depth: 10,
+ maxSize: 50000,
+ },
+ },
+ enabled: process.env.NODE_ENV === "development",
+ anonymousActionType: "Unknown",
+ stateSanitizer: (state: GlobalUIState) => {
+ return {
+ ...state,
+ notificationsApi: "",
+ contextHolder: "",
+ };
+ },
+};
+
+export const useGlobalUIBoundedStore = create()(
+ persist(
+ devtools(
+ (...a) => ({
+ ...useThemeSlice(...a),
+ ...useLoaderSlice(...a),
+ ...useStepsSlice(...a),
+ ...useTutorialSlice(...a),
+ }),
+ devtoolsOptions,
+ ),
+ {
+ name: "theme-storage",
+ storage: createJSONStorage(() => localStorage),
+ },
+ ),
+);
diff --git a/src/core/store/ui/slices/loader.slice.ts b/src/core/store/ui/slices/loader.slice.ts
new file mode 100644
index 00000000..dde0b9c1
--- /dev/null
+++ b/src/core/store/ui/slices/loader.slice.ts
@@ -0,0 +1,17 @@
+import { StateCreator } from "zustand";
+import { LoaderGlobalUIStore } from "../@types/loader.entity";
+
+export const useLoaderSlice: StateCreator<
+ LoaderGlobalUIStore,
+ [["zustand/devtools", never]],
+ [],
+ LoaderGlobalUIStore
+> = (set) => {
+ return {
+ // Stores
+ isLoading: false,
+
+ // Modifiers
+ setIsLoading: (isLoading: boolean) => set({ isLoading }),
+ };
+};
diff --git a/src/core/store/ui/slices/steps.slice.ts b/src/core/store/ui/slices/steps.slice.ts
new file mode 100644
index 00000000..d9749007
--- /dev/null
+++ b/src/core/store/ui/slices/steps.slice.ts
@@ -0,0 +1,75 @@
+import { StateCreator } from "zustand";
+import { StepsGlobalUIStore } from "../@types/steps.entity";
+
+export const useStepsSlice: StateCreator<
+ StepsGlobalUIStore,
+ [["zustand/devtools", never]],
+ [],
+ StepsGlobalUIStore
+> = (set, get) => {
+ return {
+ // Stores
+ currentStep: 1,
+ totalSteps: 1,
+ completedSteps: new Set(),
+
+ // Modifiers
+ setCurrentStep: (step: number) => {
+ if (step >= 1 && step <= get().totalSteps) {
+ set({ currentStep: step });
+ }
+ },
+
+ setTotalSteps: (total: number) => {
+ set({ totalSteps: total });
+ if (get().currentStep > total) {
+ set({ currentStep: total });
+ }
+ },
+
+ toggleStep: (step: number) => {
+ const { completedSteps, totalSteps } = get();
+ const newCompletedSteps = new Set(completedSteps);
+
+ if (newCompletedSteps.has(step)) {
+ for (let i = step; i <= totalSteps; i++) {
+ newCompletedSteps.delete(i);
+ }
+ set({
+ completedSteps: newCompletedSteps,
+ currentStep: step,
+ });
+ } else {
+ for (let i = 1; i <= step; i++) {
+ newCompletedSteps.add(i);
+ }
+ set({
+ completedSteps: newCompletedSteps,
+ currentStep: Math.min(step + 1, totalSteps),
+ });
+ }
+ },
+
+ isStepCompleted: (step: number) => {
+ return get().completedSteps.has(step);
+ },
+
+ nextStep: () => {
+ const { currentStep, totalSteps } = get();
+ if (currentStep < totalSteps) {
+ set({ currentStep: currentStep + 1 });
+ }
+ },
+
+ previousStep: () => {
+ const { currentStep } = get();
+ if (currentStep > 1) {
+ set({ currentStep: currentStep - 1 });
+ }
+ },
+
+ isFirstStep: () => get().currentStep === 1,
+ isLastStep: () => get().currentStep === get().totalSteps,
+ resetSteps: () => set({ currentStep: 0, completedSteps: new Set() }),
+ };
+};
diff --git a/src/core/store/ui/slices/theme.slice.ts b/src/core/store/ui/slices/theme.slice.ts
new file mode 100644
index 00000000..7d4b3711
--- /dev/null
+++ b/src/core/store/ui/slices/theme.slice.ts
@@ -0,0 +1,22 @@
+import { StateCreator } from "zustand";
+import { ThemeGlobalUIStore } from "../@types/theme.entity";
+
+export const useThemeSlice: StateCreator<
+ ThemeGlobalUIStore,
+ [["zustand/devtools", never]],
+ [],
+ ThemeGlobalUIStore
+> = (set, get) => {
+ return {
+ // Stores
+ theme: "light",
+
+ // Modifiers
+ toggleTheme: (newTheme?: "light" | "dark") => {
+ const currentTheme = get().theme;
+ const themeToSet =
+ newTheme || (currentTheme === "light" ? "dark" : "light");
+ set({ theme: themeToSet });
+ },
+ };
+};
diff --git a/src/core/store/ui/slices/tutorial.slice.ts b/src/core/store/ui/slices/tutorial.slice.ts
new file mode 100644
index 00000000..c77a5f3b
--- /dev/null
+++ b/src/core/store/ui/slices/tutorial.slice.ts
@@ -0,0 +1,19 @@
+import { StateCreator } from "zustand";
+import { TutorialGlobalUIStore } from "../@types/tutorial.entity";
+
+export const useTutorialSlice: StateCreator<
+ TutorialGlobalUIStore,
+ [["zustand/devtools", never]],
+ [],
+ TutorialGlobalUIStore
+> = (set) => {
+ return {
+ // Stores
+ run: false,
+
+ // Modifiers
+ setRun: (run: boolean) => {
+ set({ run });
+ },
+ };
+};
diff --git a/src/hooks/toast.hook.ts b/src/hooks/toast.hook.ts
new file mode 100644
index 00000000..87d814a5
--- /dev/null
+++ b/src/hooks/toast.hook.ts
@@ -0,0 +1,190 @@
+"use client";
+
+import * as React from "react";
+
+import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
+
+const TOAST_LIMIT = 1;
+const TOAST_REMOVE_DELAY = 250;
+
+type ToasterToast = ToastProps & {
+ id: string;
+ title?: React.ReactNode;
+ description?: React.ReactNode;
+ action?: ToastActionElement;
+};
+
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const;
+
+let count = 0;
+
+function genId() {
+ count = (count + 1) % Number.MAX_SAFE_INTEGER;
+ return count.toString();
+}
+
+type ActionType = typeof actionTypes;
+
+type Action =
+ | {
+ type: ActionType["ADD_TOAST"];
+ toast: ToasterToast;
+ }
+ | {
+ type: ActionType["UPDATE_TOAST"];
+ toast: Partial;
+ }
+ | {
+ type: ActionType["DISMISS_TOAST"];
+ toastId?: ToasterToast["id"];
+ }
+ | {
+ type: ActionType["REMOVE_TOAST"];
+ toastId?: ToasterToast["id"];
+ };
+
+interface State {
+ toasts: ToasterToast[];
+}
+
+const toastTimeouts = new Map>();
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return;
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId);
+ dispatch({
+ type: "REMOVE_TOAST",
+ toastId: toastId,
+ });
+ }, TOAST_REMOVE_DELAY);
+
+ toastTimeouts.set(toastId, timeout);
+};
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ };
+
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t,
+ ),
+ };
+
+ case "DISMISS_TOAST": {
+ const { toastId } = action;
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId);
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id);
+ });
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t,
+ ),
+ };
+ }
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ };
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ };
+ }
+};
+
+const listeners: Array<(state: State) => void> = [];
+
+let memoryState: State = { toasts: [] };
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action);
+ listeners.forEach((listener) => {
+ listener(memoryState);
+ });
+}
+
+type Toast = Omit;
+
+function toast({ ...props }: Toast) {
+ const id = genId();
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: "UPDATE_TOAST",
+ toast: { ...props, id },
+ });
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
+
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss();
+ },
+ },
+ });
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ };
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState);
+
+ React.useEffect(() => {
+ listeners.push(setState);
+ return () => {
+ const index = listeners.indexOf(setState);
+ if (index > -1) {
+ listeners.splice(index, 1);
+ }
+ };
+ }, [state]);
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ };
+}
+
+export { useToast, toast };
diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts
new file mode 100644
index 00000000..a93d5839
--- /dev/null
+++ b/src/hooks/use-mobile.ts
@@ -0,0 +1,21 @@
+import * as React from "react";
+
+const MOBILE_BREAKPOINT = 768;
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(
+ undefined,
+ );
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
+
+ return !!isMobile;
+}
diff --git a/src/utils/hook/copy.hook.ts b/src/utils/hook/copy.hook.ts
new file mode 100644
index 00000000..33692034
--- /dev/null
+++ b/src/utils/hook/copy.hook.ts
@@ -0,0 +1,23 @@
+"use client";
+
+import { useState } from "react";
+
+export const useCopyUtils = () => {
+ const [copiedKeyId, setCopiedKeyId] = useState(null);
+
+ const copyText = async (id: string | undefined, text: string | undefined) => {
+ try {
+ if (!text) throw new Error("Text is undefined");
+ await navigator.clipboard.writeText(text);
+
+ if (!id) throw new Error("Id is undefined");
+
+ setCopiedKeyId(id);
+ setTimeout(() => setCopiedKeyId(null), 2000);
+ } catch (error) {
+ console.error("Failed to copy text:", error);
+ }
+ };
+
+ return { copyText, copiedKeyId };
+};
diff --git a/src/utils/hook/format.hook.ts b/src/utils/hook/format.hook.ts
new file mode 100644
index 00000000..60bd3995
--- /dev/null
+++ b/src/utils/hook/format.hook.ts
@@ -0,0 +1,68 @@
+export const useFormatUtils = () => {
+ const formatAddress = (address: string | undefined): string => {
+ if (!address) return "";
+ const start = address.slice(0, 8);
+ const end = address.slice(-8);
+ return `${start}....${end}`;
+ };
+
+ const formatDate = () => {
+ return new Date().toLocaleDateString("en-US", {
+ month: "long",
+ year: "numeric",
+ });
+ };
+
+ const formatDateFromFirebase = (
+ seconds: number,
+ nanoseconds: number,
+ ): string => {
+ const milliseconds = seconds * 1000 + nanoseconds / 1000000;
+ const date = new Date(milliseconds);
+
+ const day = String(date.getDate()).padStart(2, "0");
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const year = date.getFullYear();
+ const hours = String(date.getHours()).padStart(2, "0");
+ const minutes = String(date.getMinutes()).padStart(2, "0");
+
+ // DD/MM/YYYY HH:MM
+ return `${day}/${month}/${year} | ${hours}:${minutes}`;
+ };
+
+ const formatDollar = (amount: string | undefined | number): string => {
+ if (!amount) return "$0.00";
+
+ const parsedAmount = parseFloat(amount.toString());
+ if (isNaN(parsedAmount)) return "$0.00";
+ return `$${parsedAmount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, "$&,")}`;
+ };
+
+ const formatText = (role: string | undefined = "") => {
+ return role
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
+ .replace(/([A-Z])/g, (match) => ` ${match}`)
+ .trim()
+ .toUpperCase();
+ };
+
+ const formatPercentage = (value: number): string => {
+ return `${value >= 0 ? "+" : ""}${value.toFixed(1)}%`;
+ };
+
+ const formatNumber = (num: number): string => {
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}m`;
+ if (num >= 1000) return `${(num / 1000).toFixed(0)}k`;
+ return num.toString();
+ };
+
+ return {
+ formatAddress,
+ formatDate,
+ formatDateFromFirebase,
+ formatDollar,
+ formatText,
+ formatPercentage,
+ formatNumber,
+ };
+};
diff --git a/src/utils/hook/input-visibility.hook.ts b/src/utils/hook/input-visibility.hook.ts
new file mode 100644
index 00000000..853ca595
--- /dev/null
+++ b/src/utils/hook/input-visibility.hook.ts
@@ -0,0 +1,16 @@
+import { Dispatch, SetStateAction } from "react";
+
+interface useChangeUtilsProps {
+ type: string;
+ setType: Dispatch>;
+}
+
+export const useChangeUtils = () => {
+ const changeTypeInput = ({ type, setType }: useChangeUtilsProps) => {
+ setType(type === "text" ? "password" : "text");
+ };
+
+ return {
+ changeTypeInput,
+ };
+};
diff --git a/src/utils/hook/valid-data.hook.ts b/src/utils/hook/valid-data.hook.ts
new file mode 100644
index 00000000..fc6bab02
--- /dev/null
+++ b/src/utils/hook/valid-data.hook.ts
@@ -0,0 +1,18 @@
+export const useValidData = () => {
+ const isValidWallet = (wallet: string) => {
+ // Verify that the wallet is 56 characters long and starts with 'G'
+ if (wallet.length !== 56 || wallet[0] !== "G") {
+ return false;
+ }
+
+ // Verify that the wallet is a valid base32 string
+ const base32Regex = /^[A-Z2-7]+$/;
+ if (!base32Regex.test(wallet)) {
+ return false;
+ }
+
+ return true;
+ };
+
+ return { isValidWallet };
+};
|