@trustless-work/blocks 0.0.9 → 1.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 (37) hide show
  1. package/bin/index.js +87 -23
  2. package/package.json +1 -1
  3. package/templates/deps.json +1 -1
  4. package/templates/escrows/details/Actions.tsx +24 -1
  5. package/templates/escrows/details/MilestoneCard.tsx +2 -2
  6. package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +0 -2
  7. package/templates/escrows/multi-release/dispute-milestone/button/{DisputeEscrow.tsx → DisputeMilestone.tsx} +4 -4
  8. package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +1 -0
  9. package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +1 -0
  10. package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +0 -1
  11. package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
  12. package/templates/escrows/multi-release/release-milestone/button/{ReleaseEscrow.tsx → ReleaseMilestone.tsx} +4 -4
  13. package/templates/escrows/multi-release/resolve-dispute/button/ResolveDispute.tsx +10 -20
  14. package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +117 -60
  15. package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +111 -55
  16. package/templates/escrows/multi-release/resolve-dispute/shared/schema.ts +68 -71
  17. package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +107 -21
  18. package/templates/escrows/multi-release/update-escrow/shared/schema.ts +0 -1
  19. package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
  20. package/templates/escrows/multi-release/withdraw-remaining-funds/button/WithdrawRemainingFunds.tsx +85 -0
  21. package/templates/escrows/multi-release/withdraw-remaining-funds/dialog/WithdrawRemainingFunds.tsx +176 -0
  22. package/templates/escrows/multi-release/withdraw-remaining-funds/form/WithdrawRemainingFunds.tsx +153 -0
  23. package/templates/escrows/multi-release/withdraw-remaining-funds/shared/schema.ts +81 -0
  24. package/templates/escrows/multi-release/withdraw-remaining-funds/shared/useWithdrawRemainingFunds.ts +160 -0
  25. package/templates/escrows/single-multi-release/approve-milestone/button/ApproveMilestone.tsx +0 -1
  26. package/templates/escrows/single-multi-release/approve-milestone/shared/useApproveMilestone.ts +0 -1
  27. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -1
  28. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
  29. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +15 -31
  30. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +116 -60
  31. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +98 -43
  32. package/templates/escrows/single-release/resolve-dispute/shared/schema.ts +65 -68
  33. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +100 -22
  34. package/templates/escrows/single-release/update-escrow/shared/schema.ts +0 -1
  35. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
  36. package/templates/tanstack/useEscrowsMutations.ts +53 -0
  37. package/templates/wallet-kit/trustlines.ts +0 -4
package/bin/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /*
4
4
  AUTHOR: @trustless-work / Joel Vargas
@@ -814,6 +814,49 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
814
814
  );
815
815
  }
816
816
 
817
+ // Post-copy: materialize shared files for multi-release withdraw-remaining-funds
818
+ try {
819
+ const isMultiWithdrawRoot =
820
+ name === "escrows/multi-release/withdraw-remaining-funds";
821
+ const isMultiWithdrawDialog =
822
+ name === "escrows/multi-release/withdraw-remaining-funds/dialog";
823
+ const isMultiWithdrawForm =
824
+ name === "escrows/multi-release/withdraw-remaining-funds/form";
825
+
826
+ const srcSharedDir = path.join(
827
+ TEMPLATES_DIR,
828
+ "escrows",
829
+ "multi-release",
830
+ "withdraw-remaining-funds",
831
+ "shared"
832
+ );
833
+
834
+ function copyMultiWithdrawSharedInto(targetDir) {
835
+ if (!fs.existsSync(srcSharedDir)) return;
836
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
837
+ for (const entry of entries) {
838
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
839
+ const entrySrc = path.join(srcSharedDir, entry.name);
840
+ const entryDest = path.join(targetDir, entry.name);
841
+ writeTransformed(entrySrc, entryDest);
842
+ }
843
+ }
844
+
845
+ if (isMultiWithdrawRoot) {
846
+ copyMultiWithdrawSharedInto(path.join(destDir, "dialog"));
847
+ copyMultiWithdrawSharedInto(path.join(destDir, "form"));
848
+ } else if (isMultiWithdrawDialog) {
849
+ copyMultiWithdrawSharedInto(destDir);
850
+ } else if (isMultiWithdrawForm) {
851
+ copyMultiWithdrawSharedInto(destDir);
852
+ }
853
+ } catch (e) {
854
+ console.warn(
855
+ "⚠️ Failed to materialize shared multi-release withdraw-remaining-funds files:",
856
+ e?.message || e
857
+ );
858
+ }
859
+
817
860
  try {
818
861
  const isMultiUpdateRoot = name === "escrows/multi-release/update-escrow";
819
862
  const isMultiUpdateDialog =
@@ -1034,6 +1077,7 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
1034
1077
  "initialize-escrow",
1035
1078
  "resolve-dispute",
1036
1079
  "update-escrow",
1080
+ "withdraw-remaining-funds",
1037
1081
  ];
1038
1082
 
1039
1083
  for (const mod of modules) {
@@ -1164,6 +1208,7 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
1164
1208
  "initialize-escrow",
1165
1209
  "resolve-dispute",
1166
1210
  "update-escrow",
1211
+ "withdraw-remaining-funds",
1167
1212
  ];
1168
1213
 
1169
1214
  const baseTarget = path.join(destDir, "multi-release");
@@ -1757,9 +1802,6 @@ if (args[0] === "init") {
1757
1802
  trustless-work add escrows/escrows-by-signer/table
1758
1803
  trustless-work add escrows/escrows-by-signer/cards
1759
1804
 
1760
- --- Escrow details (optional standalone) ---
1761
- trustless-work add escrows/details
1762
-
1763
1805
  ----------------------
1764
1806
  --- SINGLE-RELEASE ---
1765
1807
  trustless-work add escrows/single-release
@@ -1769,24 +1811,6 @@ if (args[0] === "init") {
1769
1811
  - trustless-work add escrows/single-release/initialize-escrow/form
1770
1812
  - trustless-work add escrows/single-release/initialize-escrow/dialog
1771
1813
 
1772
- --- Approve milestone ---
1773
- - trustless-work add escrows/single-release/approve-milestone
1774
- - trustless-work add escrows/single-release/approve-milestone/form
1775
- - trustless-work add escrows/single-release/approve-milestone/button
1776
- - trustless-work add escrows/single-release/approve-milestone/dialog
1777
-
1778
- --- Change milestone status ---
1779
- - trustless-work add escrows/single-release/change-milestone-status
1780
- - trustless-work add escrows/single-release/change-milestone-status/form
1781
- - trustless-work add escrows/single-release/change-milestone-status/button
1782
- - trustless-work add escrows/single-release/change-milestone-status/dialog
1783
-
1784
- --- Fund escrow ---
1785
- - trustless-work add escrows/single-release/fund-escrow
1786
- - trustless-work add escrows/single-release/fund-escrow/form
1787
- - trustless-work add escrows/single-release/fund-escrow/button
1788
- - trustless-work add escrows/single-release/fund-escrow/dialog
1789
-
1790
1814
  --- Resolve dispute ---
1791
1815
  - trustless-work add escrows/single-release/resolve-dispute
1792
1816
  - trustless-work add escrows/single-release/resolve-dispute/form
@@ -1805,9 +1829,49 @@ if (args[0] === "init") {
1805
1829
  --- Dispute escrow ---
1806
1830
  - trustless-work add escrows/single-release/dispute-escrow
1807
1831
  - trustless-work add escrows/single-release/dispute-escrow/button
1832
+
1833
+ ----------------------
1834
+ --- MULTI-RELEASE ---
1835
+ trustless-work add escrows/multi-release
1836
+
1837
+ --- Initialize escrow ---
1838
+ - trustless-work add escrows/multi-release/initialize-escrow
1839
+ - trustless-work add escrows/multi-release/initialize-escrow/form
1840
+ - trustless-work add escrows/multi-release/initialize-escrow/dialog
1841
+
1842
+ --- Resolve dispute ---
1843
+ - trustless-work add escrows/multi-release/resolve-dispute
1844
+ - trustless-work add escrows/multi-release/resolve-dispute/form
1845
+ - trustless-work add escrows/multi-release/resolve-dispute/button
1846
+ - trustless-work add escrows/multi-release/resolve-dispute/dialog
1847
+
1848
+ --- Update escrow ---
1849
+ - trustless-work add escrows/multi-release/update-escrow
1850
+ - trustless-work add escrows/multi-release/update-escrow/form
1851
+ - trustless-work add escrows/multi-release/update-escrow/dialog
1852
+
1853
+ --- Withdraw remaining funds ---
1854
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds
1855
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/form
1856
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/button
1857
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/dialog
1858
+
1859
+ --- Release escrow ---
1860
+ - trustless-work add escrows/multi-release/release-milestone
1861
+ - trustless-work add escrows/multi-release/release-milestone/button
1862
+
1863
+ --- Dispute escrow ---
1864
+ - trustless-work add escrows/multi-release/dispute-milestone
1865
+ - trustless-work add escrows/multi-release/dispute-milestone/button
1866
+
1867
+ --- Withdraw remaining funds ---
1868
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds
1869
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/form
1870
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/button
1871
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/dialog
1808
1872
 
1809
1873
  ----------------------
1810
- --- SINGLE-MULTI-RELEASE ---
1874
+ --- SINGLE-MULTI-RELEASE -> Works with both types of escrows ---
1811
1875
  trustless-work add escrows/single-multi-release
1812
1876
 
1813
1877
  --- Approve milestone ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustless-work/blocks",
3
- "version": "0.0.9",
3
+ "version": "1.0.1",
4
4
  "author": "Trustless Work",
5
5
  "keywords": [
6
6
  "react",
@@ -4,7 +4,7 @@
4
4
  "react-dom": "^18.2.0",
5
5
  "react-hook-form": "^7.53.0",
6
6
  "zod": "^3.23.8",
7
- "@trustless-work/escrow": "^2.0.2",
7
+ "@trustless-work/escrow": "^2.0.9",
8
8
  "@tanstack/react-query": "^5.75.0",
9
9
  "@tanstack/react-query-devtools": "^5.75.0",
10
10
  "tailwindcss": "^3.3.3",
@@ -113,11 +113,28 @@ export const Actions = ({
113
113
  userRolesInEscrow.includes("releaseSigner") &&
114
114
  !selectedEscrow.flags?.released;
115
115
 
116
+ const shouldShowWithdrawRemaining = (() => {
117
+ if (selectedEscrow.type !== "multi-release") return false;
118
+ if (!userRolesInEscrow.includes("disputeResolver")) return false;
119
+ if ((selectedEscrow.balance ?? 0) === 0) return false;
120
+ const milestones = (selectedEscrow.milestones || []) as Array<{
121
+ flags?: { resolved?: boolean; released?: boolean; disputed?: boolean };
122
+ }>;
123
+ return (
124
+ milestones.length > 0 &&
125
+ milestones.every((m) => {
126
+ const f = m.flags || {};
127
+ return !!(f.resolved || f.released || f.disputed);
128
+ })
129
+ );
130
+ })();
131
+
116
132
  const hasConditionalButtons =
117
133
  shouldShowEditButton ||
118
134
  shouldShowDisputeButton ||
119
135
  shouldShowResolveButton ||
120
- shouldShowReleaseFundsButton;
136
+ shouldShowReleaseFundsButton ||
137
+ shouldShowWithdrawRemaining;
121
138
 
122
139
  return (
123
140
  <div className="flex items-start justify-start flex-col gap-2 w-full">
@@ -128,13 +145,19 @@ export const Actions = ({
128
145
  {/* {shouldShowEditButton && <UpdateEscrowDialog />} */}
129
146
 
130
147
  {/* Works only with single-release escrows */}
148
+ {/* Only appears if the escrow has balance */}
131
149
  {/* {shouldShowDisputeButton && <DisputeEscrowButton />} */}
132
150
 
133
151
  {/* Works only with single-release escrows */}
152
+ {/* Only appears if the escrow is disputed */}
134
153
  {/* {shouldShowResolveButton && <ResolveDisputeDialog />} */}
135
154
 
136
155
  {/* Works only with single-release escrows */}
156
+ {/* Only appears if all the milestones are approved */}
137
157
  {/* {shouldShowReleaseFundsButton && <ReleaseEscrowButton />} */}
158
+
159
+ {/* Multi-release: Withdraw Remaining Funds */}
160
+ {/* {shouldShowWithdrawRemaining && <WithdrawRemainingFundsDialog />} */}
138
161
  </div>
139
162
  )}
140
163
 
@@ -141,7 +141,7 @@ const MilestoneCardComponent = ({
141
141
  buttons
142
142
  .push
143
143
  // You can add the button here, using the button from the blocks. This button is conditional based on the milestone status and the user roles. Works only with multi-release escrows.
144
- // <ReleaseEscrowButton
144
+ // <ReleaseMilestoneButton
145
145
  // key={`release-${milestoneIndex}`}
146
146
  // milestoneIndex={milestoneIndex}
147
147
  // />
@@ -159,7 +159,7 @@ const MilestoneCardComponent = ({
159
159
  buttons
160
160
  .push
161
161
  // You can add the button here, using the button from the blocks. This button is conditional based on the milestone status and the user roles. Works only with multi-release escrows.
162
- // <DisputeEscrowButton
162
+ // <DisputeMilestoneButton
163
163
  // key={`dispute-${milestoneIndex}`}
164
164
  // milestoneIndex={milestoneIndex}
165
165
  // />
@@ -562,5 +562,3 @@ export const EscrowsByRoleCards = () => {
562
562
  </>
563
563
  );
564
564
  };
565
-
566
- export default React.memo(EscrowsByRoleCards);
@@ -14,13 +14,13 @@ import {
14
14
  import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
15
15
  import { Loader2 } from "lucide-react";
16
16
 
17
- type DisputeEscrowButtonProps = {
17
+ type DisputeMilestoneButtonProps = {
18
18
  milestoneIndex: number | string;
19
19
  };
20
20
 
21
- export const DisputeEscrowButton = ({
21
+ export const DisputeMilestoneButton = ({
22
22
  milestoneIndex,
23
- }: DisputeEscrowButtonProps) => {
23
+ }: DisputeMilestoneButtonProps) => {
24
24
  const { startDispute } = useEscrowsMutations();
25
25
  const { selectedEscrow, updateEscrow } = useEscrowContext();
26
26
  const { walletAddress } = useWalletContext();
@@ -54,7 +54,7 @@ export const DisputeEscrowButton = ({
54
54
  address: walletAddress || "",
55
55
  });
56
56
 
57
- toast.success("Escrow disputed successfully");
57
+ toast.success("Milestone disputed successfully");
58
58
 
59
59
  updateEscrow({
60
60
  ...selectedEscrow,
@@ -482,6 +482,7 @@ export const InitializeEscrowDialog = () => {
482
482
  onClick={() => handleRemoveMilestone(index)}
483
483
  className="p-2 bg-transparent text-red-500 rounded-md border-none shadow-none hover:bg-transparent hover:shadow-none hover:text-red-500 focus:ring-0 active:ring-0 self-start sm:self-center"
484
484
  disabled={milestones.length === 1}
485
+ type="button"
485
486
  >
486
487
  <Trash2 className="h-5 w-5" />
487
488
  </Button>
@@ -462,6 +462,7 @@ export const InitializeEscrowForm = () => {
462
462
  onClick={() => handleRemoveMilestone(index)}
463
463
  className="p-2 bg-transparent text-red-500 rounded-md border-none shadow-none hover:bg-transparent hover:shadow-none hover:text-red-500 focus:ring-0 active:ring-0 self-start sm:self-center"
464
464
  disabled={milestones.length === 1}
465
+ type="button"
465
466
  >
466
467
  <Trash2 className="h-5 w-5" />
467
468
  </Button>
@@ -8,7 +8,6 @@ export const useInitializeEscrowSchema = () => {
8
8
  address: z.string().min(1, {
9
9
  message: "Trustline address is required.",
10
10
  }),
11
- decimals: z.number().default(10000000),
12
11
  }),
13
12
  roles: z.object({
14
13
  approver: z
@@ -37,7 +37,6 @@ export function useInitializeEscrow() {
37
37
  receiverMemo: "",
38
38
  trustline: {
39
39
  address: "",
40
- decimals: 10000000,
41
40
  },
42
41
  roles: {
43
42
  approver: "",
@@ -83,7 +82,6 @@ export function useInitializeEscrow() {
83
82
  receiverMemo: "123",
84
83
  trustline: {
85
84
  address: usdc?.value || "",
86
- decimals: 10000000,
87
85
  },
88
86
  roles: {
89
87
  approver: walletAddress || "",
@@ -16,13 +16,13 @@ import { useEscrowDialogs } from "@/components/tw-blocks/providers/EscrowDialogs
16
16
  import { useEscrowAmountContext } from "@/components/tw-blocks/providers/EscrowAmountProvider";
17
17
  import { Loader2 } from "lucide-react";
18
18
 
19
- type ReleaseEscrowButtonProps = {
19
+ type ReleaseMilestoneButtonProps = {
20
20
  milestoneIndex: number | string;
21
21
  };
22
22
 
23
- export const ReleaseEscrowButton = ({
23
+ export const ReleaseMilestoneButton = ({
24
24
  milestoneIndex,
25
- }: ReleaseEscrowButtonProps) => {
25
+ }: ReleaseMilestoneButtonProps) => {
26
26
  const { releaseFunds } = useEscrowsMutations();
27
27
  const { selectedEscrow, updateEscrow } = useEscrowContext();
28
28
  const dialogStates = useEscrowDialogs();
@@ -58,7 +58,7 @@ export const ReleaseEscrowButton = ({
58
58
  address: walletAddress || "",
59
59
  });
60
60
 
61
- toast.success("Escrow released successfully");
61
+ toast.success("Milestone released successfully");
62
62
 
63
63
  // Ensure amounts are up to date for the success dialog
64
64
  if (selectedEscrow) {
@@ -15,14 +15,12 @@ import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvide
15
15
  import { Loader2 } from "lucide-react";
16
16
 
17
17
  type ResolveDisputeButtonProps = {
18
- approverFunds: number;
19
- receiverFunds: number;
18
+ distributions: { address: string; amount: number }[];
20
19
  milestoneIndex: number | string;
21
20
  };
22
21
 
23
22
  export const ResolveDisputeButton = ({
24
- approverFunds,
25
- receiverFunds,
23
+ distributions,
26
24
  milestoneIndex,
27
25
  }: ResolveDisputeButtonProps) => {
28
26
  const { resolveDispute } = useEscrowsMutations();
@@ -32,18 +30,11 @@ export const ResolveDisputeButton = ({
32
30
 
33
31
  async function handleClick() {
34
32
  try {
35
- if (
36
- approverFunds == null ||
37
- Number.isNaN(approverFunds) ||
38
- receiverFunds == null ||
39
- Number.isNaN(receiverFunds)
40
- ) {
41
- toast.error("Both amounts are required");
42
- return;
43
- }
44
-
45
- if (approverFunds < 0 || receiverFunds < 0) {
46
- toast.error("Amounts must be >= 0");
33
+ const hasInvalid = distributions.some(
34
+ (d) => !d.address || Number.isNaN(d.amount) || d.amount < 0
35
+ );
36
+ if (hasInvalid) {
37
+ toast.error("Invalid distributions");
47
38
  return;
48
39
  }
49
40
 
@@ -57,8 +48,7 @@ export const ResolveDisputeButton = ({
57
48
  const payload: MultiReleaseResolveDisputePayload = {
58
49
  contractId: selectedEscrow?.contractId || "",
59
50
  disputeResolver: walletAddress || "",
60
- approverFunds: Number(approverFunds),
61
- receiverFunds: Number(receiverFunds),
51
+ distributions: distributions as [{ address: string; amount: number }],
62
52
  milestoneIndex: String(milestoneIndex),
63
53
  };
64
54
 
@@ -92,8 +82,8 @@ export const ResolveDisputeButton = ({
92
82
  return milestone;
93
83
  }),
94
84
  balance:
95
- selectedEscrow?.balance ||
96
- Number(approverFunds) + Number(receiverFunds),
85
+ (selectedEscrow?.balance || 0) -
86
+ distributions.reduce((acc, d) => acc + Number(d.amount || 0), 0),
97
87
  });
98
88
  } catch (error) {
99
89
  toast.error(handleError(error as ErrorResponse).message);
@@ -16,7 +16,7 @@ import {
16
16
  DialogTitle,
17
17
  DialogTrigger,
18
18
  } from "__UI_BASE__/dialog";
19
- import { Loader2 } from "lucide-react";
19
+ import { Loader2, Trash2 } from "lucide-react";
20
20
  import { useResolveDispute } from "./useResolveDispute";
21
21
  import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
22
22
  import { formatCurrency } from "../../../../helpers/format.helper";
@@ -35,7 +35,22 @@ export const ResolveDisputeDialog = ({
35
35
  showSelectMilestone?: boolean;
36
36
  milestoneIndex?: number | string;
37
37
  }) => {
38
- const { form, handleSubmit, isSubmitting, totalAmount } = useResolveDispute();
38
+ const {
39
+ form,
40
+ handleSubmit,
41
+ isSubmitting,
42
+ totalAmount,
43
+ distributions,
44
+ handleAddDistribution,
45
+ handleRemoveDistribution,
46
+ handleDistributionAddressChange,
47
+ handleDistributionAmountChange,
48
+ isAnyDistributionEmpty,
49
+ allowedAmount,
50
+ distributedSum,
51
+ isExactMatch,
52
+ difference,
53
+ } = useResolveDispute();
39
54
  const { selectedEscrow } = useEscrowContext();
40
55
 
41
56
  React.useEffect(() => {
@@ -55,7 +70,7 @@ export const ResolveDisputeDialog = ({
55
70
  Resolve Dispute
56
71
  </Button>
57
72
  </DialogTrigger>
58
- <DialogContent>
73
+ <DialogContent className="!w-full sm:!max-w-3xl max-h-[95vh] overflow-y-auto">
59
74
  <DialogHeader>
60
75
  <DialogTitle>Resolve Dispute</DialogTitle>
61
76
  </DialogHeader>
@@ -96,52 +111,110 @@ export const ResolveDisputeDialog = ({
96
111
  />
97
112
  )}
98
113
 
99
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
100
- <FormField
101
- control={form.control}
102
- name="approverFunds"
103
- render={({ field }) => (
104
- <FormItem>
105
- <FormLabel>Approver Funds</FormLabel>
106
- <FormControl>
107
- <Input
108
- type="text"
109
- inputMode="decimal"
110
- placeholder="Enter approver funds"
111
- value={field.value as unknown as string}
112
- onChange={(e) => field.onChange(e.target.value)}
113
- />
114
- </FormControl>
115
- <FormMessage />
116
- </FormItem>
117
- )}
118
- />
114
+ <FormLabel className="flex items-center my-4">
115
+ Distributions<span className="text-destructive ml-1">*</span>
116
+ </FormLabel>
119
117
 
120
- <FormField
121
- control={form.control}
122
- name="receiverFunds"
123
- render={({ field }) => (
124
- <FormItem>
125
- <FormLabel>Receiver Funds</FormLabel>
126
- <FormControl>
127
- <Input
128
- type="text"
129
- inputMode="decimal"
130
- placeholder="Enter receiver funds"
131
- value={field.value as unknown as string}
132
- onChange={(e) => field.onChange(e.target.value)}
133
- />
134
- </FormControl>
135
- <FormMessage />
136
- </FormItem>
137
- )}
138
- />
118
+ {distributions.map((d, idx) => (
119
+ <div
120
+ key={`dist-${idx}`}
121
+ className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-[1fr_minmax(140px,220px)_auto] gap-3 sm:gap-4 items-end mb-2"
122
+ >
123
+ <FormField
124
+ control={form.control}
125
+ name={`distributions.${idx}.address` as const}
126
+ render={() => (
127
+ <FormItem className="sm:col-span-2 lg:col-span-1">
128
+ <FormLabel>Address</FormLabel>
129
+ <FormControl>
130
+ <Input
131
+ type="text"
132
+ placeholder="Receiver address"
133
+ value={d.address}
134
+ onChange={(e) =>
135
+ handleDistributionAddressChange(idx, e.target.value)
136
+ }
137
+ />
138
+ </FormControl>
139
+ <FormMessage />
140
+ </FormItem>
141
+ )}
142
+ />
143
+
144
+ <FormField
145
+ control={form.control}
146
+ name={`distributions.${idx}.amount` as const}
147
+ render={() => (
148
+ <FormItem>
149
+ <FormLabel>Amount</FormLabel>
150
+ <FormControl>
151
+ <Input
152
+ type="text"
153
+ inputMode="decimal"
154
+ placeholder="0.00"
155
+ value={(d.amount as string) ?? ""}
156
+ onChange={(e) =>
157
+ handleDistributionAmountChange(idx, e)
158
+ }
159
+ />
160
+ </FormControl>
161
+ <FormMessage />
162
+ </FormItem>
163
+ )}
164
+ />
165
+
166
+ <Button
167
+ type="button"
168
+ onClick={() => handleRemoveDistribution(idx)}
169
+ className="justify-self-end self-end p-2 bg-transparent text-destructive rounded-md border-none shadow-none hover:bg-transparent hover:shadow-none hover:text-destructive focus:ring-0 active:ring-0"
170
+ disabled={distributions.length <= 2}
171
+ >
172
+ <Trash2 className="h-5 w-5" />
173
+ </Button>
174
+ </div>
175
+ ))}
176
+
177
+ <div className="flex justify-between items-center mt-4">
178
+ <Button
179
+ type="button"
180
+ variant="outline"
181
+ onClick={handleAddDistribution}
182
+ disabled={isAnyDistributionEmpty}
183
+ className="cursor-pointer"
184
+ >
185
+ Add Item
186
+ </Button>
187
+
188
+ <div className="flex items-center gap-4">
189
+ <div className="text-xs text-muted-foreground">
190
+ <p>
191
+ <span className="font-bold">Total Amount: </span>
192
+ {distributedSum.toFixed(2)} / {allowedAmount.toFixed(2)}
193
+ </p>
194
+ {!isExactMatch && (
195
+ <p className="text-destructive">
196
+ <span className="font-bold">Difference: </span>
197
+ {difference.toFixed(2)}
198
+ </p>
199
+ )}
200
+ </div>
201
+
202
+ <p className="text-xs text-muted-foreground">
203
+ <span className="font-bold">Total Balance: </span>
204
+ {formatCurrency(
205
+ selectedEscrow?.balance || 0,
206
+ selectedEscrow?.trustline.name || ""
207
+ )}
208
+ </p>
209
+ </div>
139
210
  </div>
140
211
 
141
- <div className="mt-4 flex justify-between items-center">
212
+ <div className="mt-4 flex justify-start items-center">
142
213
  <Button
143
214
  type="submit"
144
- disabled={isSubmitting}
215
+ disabled={
216
+ isSubmitting || isAnyDistributionEmpty || !isExactMatch
217
+ }
145
218
  className="cursor-pointer"
146
219
  >
147
220
  {isSubmitting ? (
@@ -153,22 +226,6 @@ export const ResolveDisputeDialog = ({
153
226
  "Resolve"
154
227
  )}
155
228
  </Button>
156
-
157
- <p className="text-xs text-muted-foreground">
158
- <span className="font-bold">Total Amount: </span>
159
- {formatCurrency(
160
- totalAmount,
161
- selectedEscrow?.trustline.name || ""
162
- )}
163
- </p>
164
-
165
- <p className="text-xs text-muted-foreground">
166
- <span className="font-bold">Total Balance: </span>
167
- {formatCurrency(
168
- selectedEscrow?.balance || 0,
169
- selectedEscrow?.trustline.name || ""
170
- )}
171
- </p>
172
229
  </div>
173
230
  </form>
174
231
  </Form>