@trustless-work/blocks 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +96 -0
  2. package/bin/index.js +1123 -0
  3. package/package.json +44 -0
  4. package/templates/deps.json +29 -0
  5. package/templates/escrows/details/Actions.tsx +149 -0
  6. package/templates/escrows/details/Entities.tsx +48 -0
  7. package/templates/escrows/details/EntityCard.tsx +98 -0
  8. package/templates/escrows/details/EscrowDetailDialog.tsx +154 -0
  9. package/templates/escrows/details/GeneralInformation.tsx +329 -0
  10. package/templates/escrows/details/MilestoneCard.tsx +254 -0
  11. package/templates/escrows/details/MilestoneDetailDialog.tsx +276 -0
  12. package/templates/escrows/details/Milestones.tsx +87 -0
  13. package/templates/escrows/details/ProgressEscrow.tsx +191 -0
  14. package/templates/escrows/details/StatisticsCard.tsx +79 -0
  15. package/templates/escrows/details/SuccessReleaseDialog.tsx +101 -0
  16. package/templates/escrows/details/useDetailsEscrow.ts +126 -0
  17. package/templates/escrows/escrow-context/EscrowAmountProvider.tsx +86 -0
  18. package/templates/escrows/escrow-context/EscrowDialogsProvider.tsx +108 -0
  19. package/templates/escrows/escrow-context/EscrowProvider.tsx +124 -0
  20. package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +503 -0
  21. package/templates/escrows/escrows-by-role/cards/Filters.tsx +421 -0
  22. package/templates/escrows/escrows-by-role/table/EscrowsTable.tsx +427 -0
  23. package/templates/escrows/escrows-by-role/table/Filters.tsx +421 -0
  24. package/templates/escrows/escrows-by-role/useEscrowsByRole.shared.ts +336 -0
  25. package/templates/escrows/escrows-by-signer/cards/EscrowsCards.tsx +502 -0
  26. package/templates/escrows/escrows-by-signer/cards/Filters.tsx +389 -0
  27. package/templates/escrows/escrows-by-signer/table/EscrowsTable.tsx +422 -0
  28. package/templates/escrows/escrows-by-signer/table/Filters.tsx +389 -0
  29. package/templates/escrows/escrows-by-signer/useEscrowsBySigner.shared.ts +320 -0
  30. package/templates/escrows/single-release/approve-milestone/button/ApproveMilestone.tsx +78 -0
  31. package/templates/escrows/single-release/approve-milestone/dialog/ApproveMilestone.tsx +102 -0
  32. package/templates/escrows/single-release/approve-milestone/form/ApproveMilestone.tsx +80 -0
  33. package/templates/escrows/single-release/approve-milestone/shared/schema.ts +9 -0
  34. package/templates/escrows/single-release/approve-milestone/shared/useApproveMilestone.ts +67 -0
  35. package/templates/escrows/single-release/change-milestone-status/button/ChangeMilestoneStatus.tsx +78 -0
  36. package/templates/escrows/single-release/change-milestone-status/dialog/ChangeMilestoneStatus.tsx +167 -0
  37. package/templates/escrows/single-release/change-milestone-status/form/ChangeMilestoneStatus.tsx +114 -0
  38. package/templates/escrows/single-release/change-milestone-status/shared/schema.ts +15 -0
  39. package/templates/escrows/single-release/change-milestone-status/shared/useChangeMilestoneStatus.ts +77 -0
  40. package/templates/escrows/single-release/dispute-escrow/button/DisputeEscrow.tsx +68 -0
  41. package/templates/escrows/single-release/fund-escrow/button/FundEscrow.tsx +84 -0
  42. package/templates/escrows/single-release/fund-escrow/dialog/FundEscrow.tsx +77 -0
  43. package/templates/escrows/single-release/fund-escrow/form/FundEscrow.tsx +54 -0
  44. package/templates/escrows/single-release/fund-escrow/shared/schema.ts +10 -0
  45. package/templates/escrows/single-release/fund-escrow/shared/useFundEscrow.ts +66 -0
  46. package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +526 -0
  47. package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +504 -0
  48. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +232 -0
  49. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +115 -0
  50. package/templates/escrows/single-release/release-escrow/button/ReleaseEscrow.tsx +80 -0
  51. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +94 -0
  52. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +123 -0
  53. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +82 -0
  54. package/templates/escrows/single-release/resolve-dispute/shared/schema.ts +82 -0
  55. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +58 -0
  56. package/templates/escrows/single-release/update-escrow/dialog/UpdateEscrow.tsx +485 -0
  57. package/templates/escrows/single-release/update-escrow/form/UpdateEscrow.tsx +463 -0
  58. package/templates/escrows/single-release/update-escrow/shared/schema.ts +139 -0
  59. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +211 -0
  60. package/templates/handle-errors/errors.enum.ts +6 -0
  61. package/templates/handle-errors/handle.ts +47 -0
  62. package/templates/helpers/format.helper.ts +27 -0
  63. package/templates/helpers/useCopy.ts +13 -0
  64. package/templates/providers/ReactQueryClientProvider.tsx +28 -0
  65. package/templates/providers/TrustlessWork.tsx +30 -0
  66. package/templates/tanstak/useEscrowsByRoleQuery.ts +87 -0
  67. package/templates/tanstak/useEscrowsBySignerQuery.ts +78 -0
  68. package/templates/tanstak/useEscrowsMutations.ts +411 -0
  69. package/templates/wallet-kit/WalletButtons.tsx +116 -0
  70. package/templates/wallet-kit/WalletProvider.tsx +94 -0
  71. package/templates/wallet-kit/trustlines.ts +40 -0
  72. package/templates/wallet-kit/useWallet.ts +77 -0
  73. package/templates/wallet-kit/validators.ts +12 -0
  74. package/templates/wallet-kit/wallet-kit.ts +30 -0
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import React, { useMemo } from "react";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from "__UI_BASE__/dialog";
11
+ import { Card } from "__UI_BASE__/card";
12
+ import EntityCard from "./EntityCard";
13
+ import { useEscrowContext } from "../../escrow-context/EscrowProvider";
14
+ import { useEscrowAmountContext } from "../../escrow-context/EscrowAmountProvider";
15
+ import { CircleCheckBig } from "lucide-react";
16
+
17
+ interface SuccessReleaseDialogProps {
18
+ isOpen: boolean;
19
+ onOpenChange: (open: boolean) => void;
20
+ }
21
+
22
+ export function SuccessReleaseDialog({
23
+ isOpen,
24
+ onOpenChange,
25
+ }: SuccessReleaseDialogProps) {
26
+ const { selectedEscrow } = useEscrowContext();
27
+ const { receiverAmount, platformFeeAmount, trustlessWorkAmount } =
28
+ useEscrowAmountContext();
29
+
30
+ const platformFee = Number(selectedEscrow?.platformFee || 0);
31
+ const trustlessPercentage = 0.3;
32
+ const receiverPercentage = 100 - (platformFee + trustlessPercentage);
33
+
34
+ const currency = selectedEscrow?.trustline?.name ?? "";
35
+
36
+ const cards = useMemo(
37
+ () => [
38
+ {
39
+ type: "Platform",
40
+ entity: selectedEscrow?.roles?.platformAddress,
41
+ percentage: platformFee,
42
+ amount: platformFeeAmount,
43
+ },
44
+ {
45
+ type: "Trustless Work",
46
+ entity: "Private",
47
+ percentage: trustlessPercentage,
48
+ amount: trustlessWorkAmount,
49
+ },
50
+ {
51
+ type: "Receiver",
52
+ entity: selectedEscrow?.roles?.receiver,
53
+ percentage: receiverPercentage,
54
+ amount: receiverAmount,
55
+ },
56
+ ],
57
+ [
58
+ platformFee,
59
+ receiverPercentage,
60
+ trustlessPercentage,
61
+ platformFeeAmount,
62
+ trustlessWorkAmount,
63
+ receiverAmount,
64
+ selectedEscrow?.roles?.platformAddress,
65
+ selectedEscrow?.roles?.receiver,
66
+ ]
67
+ );
68
+
69
+ return (
70
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
71
+ <DialogContent className="max-w-md sm:max-w-lg md:max-w-2xl">
72
+ <DialogHeader>
73
+ <DialogTitle className="flex items-center gap-2">
74
+ <CircleCheckBig className="h-5 w-5 text-green-600" />
75
+ Release Successful
76
+ </DialogTitle>
77
+ <DialogDescription>
78
+ Funds were distributed successfully to the corresponding parties.
79
+ </DialogDescription>
80
+ </DialogHeader>
81
+
82
+ <div className="flex flex-col gap-3">
83
+ {cards.map((c) => (
84
+ <EntityCard
85
+ key={c.type}
86
+ type={c.type}
87
+ entity={c.entity}
88
+ hasPercentage
89
+ percentage={Number(c.percentage.toFixed(2))}
90
+ hasAmount
91
+ amount={Number(c.amount.toFixed(2))}
92
+ currency={currency}
93
+ />
94
+ ))}
95
+ </div>
96
+ </DialogContent>
97
+ </Dialog>
98
+ );
99
+ }
100
+
101
+ export default SuccessReleaseDialog;
@@ -0,0 +1,126 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types";
3
+ import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
4
+ import { useEscrowContext } from "../../escrow-context/EscrowProvider";
5
+ import { useEscrowAmountContext } from "../../escrow-context/EscrowAmountProvider";
6
+
7
+ interface EscrowDetailDialogProps {
8
+ setIsDialogOpen: (value: boolean) => void;
9
+ setSelectedEscrow: (selectedEscrow?: Escrow) => void;
10
+ selectedEscrow: Escrow | null;
11
+ }
12
+
13
+ const useEscrowDetailDialog = ({
14
+ setIsDialogOpen,
15
+ setSelectedEscrow,
16
+ selectedEscrow,
17
+ }: EscrowDetailDialogProps) => {
18
+ const { walletAddress } = useWalletContext();
19
+ const { userRolesInEscrow, setUserRolesInEscrow } = useEscrowContext();
20
+
21
+ const { setAmounts } = useEscrowAmountContext();
22
+
23
+ const fetchingRef = useRef(false);
24
+ const lastFetchKey = useRef("");
25
+ const [evidenceVisibleMap, setEvidenceVisibleMap] = useState<{
26
+ [key: number]: boolean;
27
+ }>({});
28
+
29
+ const totalAmount = Number(selectedEscrow?.amount || 0);
30
+ const platformFeePercentage = Number(selectedEscrow?.platformFee || 0);
31
+
32
+ const handleClose = useCallback(() => {
33
+ setIsDialogOpen(false);
34
+ setSelectedEscrow(undefined);
35
+ }, [setIsDialogOpen, setSelectedEscrow]);
36
+
37
+ const areAllMilestonesApproved =
38
+ selectedEscrow?.milestones?.every((milestone) => {
39
+ if ("flags" in milestone) {
40
+ return milestone.flags?.approved === true;
41
+ }
42
+ return "approved" in milestone && milestone.approved === true;
43
+ }) ?? false;
44
+
45
+ const fetchUserRoleInEscrow = useCallback(async () => {
46
+ if (!selectedEscrow?.contractId || !walletAddress) return null;
47
+
48
+ const roleMappings = [
49
+ { name: "approver", address: selectedEscrow.roles.approver },
50
+ {
51
+ name: "serviceProvider",
52
+ address: selectedEscrow.roles.serviceProvider,
53
+ },
54
+ {
55
+ name: "platformAddress",
56
+ address: selectedEscrow.roles.platformAddress,
57
+ },
58
+ { name: "releaseSigner", address: selectedEscrow.roles.releaseSigner },
59
+ {
60
+ name: "disputeResolver",
61
+ address: selectedEscrow.roles.disputeResolver,
62
+ },
63
+ { name: "receiver", address: selectedEscrow.roles.receiver },
64
+ ];
65
+
66
+ const userRoles = roleMappings
67
+ .filter((role) => role.address === walletAddress)
68
+ .map((role) => role.name);
69
+
70
+ return userRoles;
71
+ }, [selectedEscrow?.contractId, walletAddress]);
72
+
73
+ useEffect(() => {
74
+ let timeoutId: NodeJS.Timeout | undefined = undefined;
75
+ let isMounted = true;
76
+
77
+ const fetchRoles = async () => {
78
+ if (!selectedEscrow || !walletAddress || fetchingRef.current) return;
79
+
80
+ const fetchKey = `${selectedEscrow.contractId}-${walletAddress}`;
81
+ if (fetchKey === lastFetchKey.current) return;
82
+
83
+ try {
84
+ fetchingRef.current = true;
85
+ lastFetchKey.current = fetchKey;
86
+ const roleData = await fetchUserRoleInEscrow();
87
+ if (isMounted && roleData) {
88
+ setUserRolesInEscrow(roleData);
89
+ }
90
+ } catch (error) {
91
+ console.error("[EscrowDetailDialog] Error fetching roles:", error);
92
+ } finally {
93
+ fetchingRef.current = false;
94
+ }
95
+ };
96
+
97
+ timeoutId = setTimeout(fetchRoles, 100);
98
+
99
+ return () => {
100
+ isMounted = false;
101
+ if (timeoutId) {
102
+ clearTimeout(timeoutId);
103
+ }
104
+ fetchingRef.current = false;
105
+ };
106
+ }, [
107
+ selectedEscrow,
108
+ fetchUserRoleInEscrow,
109
+ setUserRolesInEscrow,
110
+ walletAddress,
111
+ ]);
112
+
113
+ useEffect(() => {
114
+ setAmounts(totalAmount, platformFeePercentage);
115
+ }, [totalAmount, platformFeePercentage, setAmounts]);
116
+
117
+ return {
118
+ handleClose,
119
+ setEvidenceVisibleMap,
120
+ evidenceVisibleMap,
121
+ areAllMilestonesApproved,
122
+ userRolesInEscrow,
123
+ };
124
+ };
125
+
126
+ export default useEscrowDetailDialog;
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ createContext,
6
+ useContext,
7
+ useMemo,
8
+ useState,
9
+ ReactNode,
10
+ useCallback,
11
+ } from "react";
12
+
13
+ export type AmountEscrowStore = {
14
+ receiverAmount: number;
15
+ platformFeeAmount: number;
16
+ trustlessWorkAmount: number;
17
+ receiverResolve: number;
18
+ approverResolve: number;
19
+ amountMoonpay: number;
20
+ setAmounts: (totalAmount: number, platformFee: number) => void;
21
+ setReceiverResolve: (value: number) => void;
22
+ setApproverResolve: (value: number) => void;
23
+ setAmountMoonpay: (value: number) => void;
24
+ };
25
+
26
+ const EscrowAmountContext = createContext<AmountEscrowStore | undefined>(
27
+ undefined
28
+ );
29
+
30
+ export const EscrowAmountProvider = ({ children }: { children: ReactNode }) => {
31
+ const [receiverAmount, setReceiverAmount] = useState(0);
32
+ const [platformFeeAmount, setPlatformFeeAmount] = useState(0);
33
+ const [trustlessWorkAmount, setTrustlessWorkAmount] = useState(0);
34
+ const [receiverResolve, setReceiverResolve] = useState(0);
35
+ const [approverResolve, setApproverResolve] = useState(0);
36
+ const [amountMoonpay, setAmountMoonpay] = useState(0);
37
+
38
+ const setAmounts: AmountEscrowStore["setAmounts"] = useCallback(
39
+ (totalAmount, platformFee) => {
40
+ const trustlessPercentage = 0.3;
41
+ const receiverPercentage = 100 - (trustlessPercentage + platformFee);
42
+
43
+ setReceiverAmount((totalAmount * receiverPercentage) / 100);
44
+ setPlatformFeeAmount((totalAmount * platformFee) / 100);
45
+ setTrustlessWorkAmount((totalAmount * trustlessPercentage) / 100);
46
+ },
47
+ []
48
+ );
49
+
50
+ const value = useMemo<AmountEscrowStore>(
51
+ () => ({
52
+ receiverAmount,
53
+ platformFeeAmount,
54
+ trustlessWorkAmount,
55
+ receiverResolve,
56
+ approverResolve,
57
+ amountMoonpay,
58
+ setAmounts,
59
+ setReceiverResolve,
60
+ setApproverResolve,
61
+ setAmountMoonpay,
62
+ }),
63
+ [
64
+ receiverAmount,
65
+ platformFeeAmount,
66
+ trustlessWorkAmount,
67
+ receiverResolve,
68
+ approverResolve,
69
+ amountMoonpay,
70
+ ]
71
+ );
72
+
73
+ return (
74
+ <EscrowAmountContext.Provider value={value}>
75
+ {children}
76
+ </EscrowAmountContext.Provider>
77
+ );
78
+ };
79
+
80
+ export function useEscrowAmountContext() {
81
+ const ctx = useContext(EscrowAmountContext);
82
+ if (!ctx) {
83
+ throw new Error("useEscrowAmount must be used within EscrowAmountProvider");
84
+ }
85
+ return ctx;
86
+ }
@@ -0,0 +1,108 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { createContext, useContext, useMemo, useState } from "react";
5
+
6
+ export type DialogState = {
7
+ isOpen: boolean;
8
+ setIsOpen: (open: boolean) => void;
9
+ };
10
+
11
+ export type DialogStates = {
12
+ second: DialogState;
13
+ completeMilestone: DialogState;
14
+ qr: DialogState;
15
+ resolveDispute: DialogState;
16
+ editMilestone: DialogState;
17
+ editEntities: DialogState;
18
+ editBasicProperties: DialogState;
19
+ successRelease: DialogState;
20
+ successResolveDispute: DialogState;
21
+ };
22
+
23
+ export type StatusStates = {};
24
+
25
+ type EscrowDialogsContextType = DialogStates & StatusStates;
26
+
27
+ const EscrowDialogsContext = createContext<
28
+ EscrowDialogsContextType | undefined
29
+ >(undefined);
30
+
31
+ export function EscrowDialogsProvider({
32
+ children,
33
+ }: {
34
+ children: React.ReactNode;
35
+ }) {
36
+ const [secondOpen, setSecondOpen] = useState(false);
37
+ const [completeMilestoneOpen, setCompleteMilestoneOpen] = useState(false);
38
+ const [qrOpen, setQrOpen] = useState(false);
39
+ const [resolveDisputeOpen, setResolveDisputeOpen] = useState(false);
40
+ const [editMilestoneOpen, setEditMilestoneOpen] = useState(false);
41
+ const [editEntitiesOpen, setEditEntitiesOpen] = useState(false);
42
+ const [editBasicPropertiesOpen, setEditBasicPropertiesOpen] = useState(false);
43
+ const [successReleaseOpen, setSuccessReleaseOpen] = useState(false);
44
+ const [successResolveDisputeOpen, setSuccessResolveDisputeOpen] =
45
+ useState(false);
46
+
47
+ const value = useMemo<EscrowDialogsContextType>(
48
+ () => ({
49
+ second: { isOpen: secondOpen, setIsOpen: setSecondOpen },
50
+ completeMilestone: {
51
+ isOpen: completeMilestoneOpen,
52
+ setIsOpen: setCompleteMilestoneOpen,
53
+ },
54
+ qr: { isOpen: qrOpen, setIsOpen: setQrOpen },
55
+ resolveDispute: {
56
+ isOpen: resolveDisputeOpen,
57
+ setIsOpen: setResolveDisputeOpen,
58
+ },
59
+ editMilestone: {
60
+ isOpen: editMilestoneOpen,
61
+ setIsOpen: setEditMilestoneOpen,
62
+ },
63
+ editEntities: {
64
+ isOpen: editEntitiesOpen,
65
+ setIsOpen: setEditEntitiesOpen,
66
+ },
67
+ editBasicProperties: {
68
+ isOpen: editBasicPropertiesOpen,
69
+ setIsOpen: setEditBasicPropertiesOpen,
70
+ },
71
+ successRelease: {
72
+ isOpen: successReleaseOpen,
73
+ setIsOpen: setSuccessReleaseOpen,
74
+ },
75
+ successResolveDispute: {
76
+ isOpen: successResolveDisputeOpen,
77
+ setIsOpen: setSuccessResolveDisputeOpen,
78
+ },
79
+ }),
80
+ [
81
+ secondOpen,
82
+ completeMilestoneOpen,
83
+ qrOpen,
84
+ resolveDisputeOpen,
85
+ editMilestoneOpen,
86
+ editEntitiesOpen,
87
+ editBasicPropertiesOpen,
88
+ successReleaseOpen,
89
+ successResolveDisputeOpen,
90
+ ]
91
+ );
92
+
93
+ return (
94
+ <EscrowDialogsContext.Provider value={value}>
95
+ {children}
96
+ </EscrowDialogsContext.Provider>
97
+ );
98
+ }
99
+
100
+ export function useEscrowDialogs() {
101
+ const ctx = useContext(EscrowDialogsContext);
102
+ if (!ctx) {
103
+ throw new Error(
104
+ "useEscrowDialogs must be used within EscrowDialogsProvider"
105
+ );
106
+ }
107
+ return ctx;
108
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ createContext,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ ReactNode,
11
+ useCallback,
12
+ } from "react";
13
+ import { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types";
14
+
15
+ type EscrowContextType = {
16
+ selectedEscrow: Escrow | null;
17
+ hasEscrow: boolean;
18
+ userRolesInEscrow: string[];
19
+ updateEscrow: (
20
+ updater: Partial<Escrow> | ((previous: Escrow) => Escrow)
21
+ ) => void;
22
+ setEscrowField: <K extends keyof Escrow>(key: K, value: Escrow[K]) => void;
23
+ clearEscrow: () => void;
24
+ setSelectedEscrow: (escrow?: Escrow) => void;
25
+ setUserRolesInEscrow: (roles: string[]) => void;
26
+ };
27
+
28
+ const EscrowContext = createContext<EscrowContextType | undefined>(undefined);
29
+
30
+ const LOCAL_STORAGE_KEY = "selectedEscrow";
31
+
32
+ export const EscrowProvider = ({ children }: { children: ReactNode }) => {
33
+ const [selectedEscrow, setSelectedEscrowState] = useState<Escrow | null>(
34
+ null
35
+ );
36
+ const [userRolesInEscrow, setUserRolesInEscrowState] = useState<string[]>([]);
37
+
38
+ useEffect(() => {
39
+ try {
40
+ const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
41
+ if (stored) {
42
+ const parsed: Escrow = JSON.parse(stored);
43
+ setSelectedEscrowState(parsed);
44
+ }
45
+ } catch (_err) {
46
+ // ignore malformed localStorage content
47
+ }
48
+ }, []);
49
+
50
+ const persist = (value: Escrow | null) => {
51
+ if (value) {
52
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(value));
53
+ } else {
54
+ localStorage.removeItem(LOCAL_STORAGE_KEY);
55
+ }
56
+ };
57
+
58
+ const updateEscrow: EscrowContextType["updateEscrow"] = (updater) => {
59
+ setSelectedEscrowState((current) => {
60
+ if (!current) return current;
61
+ const next =
62
+ typeof updater === "function"
63
+ ? updater(current)
64
+ : { ...current, ...updater };
65
+ persist(next);
66
+ return next;
67
+ });
68
+ };
69
+
70
+ const setEscrowField: EscrowContextType["setEscrowField"] = (key, value) => {
71
+ setSelectedEscrowState((current) => {
72
+ if (!current) return current;
73
+ const next = { ...current, [key]: value } as Escrow;
74
+ persist(next);
75
+ return next;
76
+ });
77
+ };
78
+
79
+ const clearEscrow = () => {
80
+ setSelectedEscrowState(null);
81
+ persist(null);
82
+ };
83
+
84
+ const setUserRolesInEscrow = useCallback((roles: string[]) => {
85
+ setUserRolesInEscrowState((prev) => {
86
+ // Avoid unnecessary updates to prevent re-renders
87
+ if (
88
+ prev.length === roles.length &&
89
+ prev.every((r, i) => r === roles[i])
90
+ ) {
91
+ return prev;
92
+ }
93
+ return roles;
94
+ });
95
+ }, []);
96
+
97
+ const hasEscrow = useMemo(() => Boolean(selectedEscrow), [selectedEscrow]);
98
+
99
+ return (
100
+ <EscrowContext.Provider
101
+ value={{
102
+ selectedEscrow,
103
+ hasEscrow,
104
+ updateEscrow,
105
+ setEscrowField,
106
+ clearEscrow,
107
+ setSelectedEscrow: (value?: Escrow) =>
108
+ setSelectedEscrowState(value ?? null),
109
+ setUserRolesInEscrow,
110
+ userRolesInEscrow,
111
+ }}
112
+ >
113
+ {children}
114
+ </EscrowContext.Provider>
115
+ );
116
+ };
117
+
118
+ export const useEscrowContext = () => {
119
+ const context = useContext(EscrowContext);
120
+ if (!context) {
121
+ throw new Error("useEscrowContext must be used within EscrowProvider");
122
+ }
123
+ return context;
124
+ };