@trustless-work/blocks 1.0.0 → 1.0.2

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 (36) hide show
  1. package/bin/index.js +78 -1
  2. package/package.json +1 -1
  3. package/templates/deps.json +1 -1
  4. package/templates/escrows/details/Actions.tsx +21 -1
  5. package/templates/escrows/indicators/balance-progress/bar/BalanceProgress.tsx +55 -0
  6. package/templates/escrows/indicators/balance-progress/donut/BalanceProgress.tsx +99 -0
  7. package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +1 -0
  8. package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +1 -0
  9. package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +0 -1
  10. package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
  11. package/templates/escrows/multi-release/resolve-dispute/button/ResolveDispute.tsx +10 -20
  12. package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +117 -60
  13. package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +111 -55
  14. package/templates/escrows/multi-release/resolve-dispute/shared/schema.ts +68 -71
  15. package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +107 -21
  16. package/templates/escrows/multi-release/update-escrow/shared/schema.ts +0 -1
  17. package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
  18. package/templates/escrows/multi-release/withdraw-remaining-funds/button/WithdrawRemainingFunds.tsx +85 -0
  19. package/templates/escrows/multi-release/withdraw-remaining-funds/dialog/WithdrawRemainingFunds.tsx +176 -0
  20. package/templates/escrows/multi-release/withdraw-remaining-funds/form/WithdrawRemainingFunds.tsx +153 -0
  21. package/templates/escrows/multi-release/withdraw-remaining-funds/shared/schema.ts +81 -0
  22. package/templates/escrows/multi-release/withdraw-remaining-funds/shared/useWithdrawRemainingFunds.ts +160 -0
  23. package/templates/escrows/single-multi-release/approve-milestone/button/ApproveMilestone.tsx +0 -1
  24. package/templates/escrows/single-multi-release/approve-milestone/shared/useApproveMilestone.ts +0 -1
  25. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -1
  26. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
  27. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +15 -31
  28. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +116 -60
  29. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +98 -43
  30. package/templates/escrows/single-release/resolve-dispute/shared/schema.ts +65 -68
  31. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +100 -22
  32. package/templates/escrows/single-release/update-escrow/shared/schema.ts +0 -1
  33. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
  34. package/templates/tanstack/useEscrowsMutations.ts +53 -0
  35. package/templates/tanstack/useGetMultipleEscrowBalances.ts +41 -0
  36. package/templates/wallet-kit/trustlines.ts +0 -4
package/bin/index.js CHANGED
@@ -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");
@@ -1535,7 +1580,7 @@ if (args[0] === "init") {
1535
1580
  }
1536
1581
 
1537
1582
  const addShadcn = await promptYesNo(
1538
- "Add shadcn components (button, input, form, card, sonner, checkbox, dialog, textarea, sonner, select, table, calendar, popover, separator, calendar-05, badge, sheet, tabs, avatar, tooltip)?",
1583
+ "Add shadcn components (button, input, form, card, sonner, checkbox, dialog, textarea, sonner, select, table, calendar, popover, separator, calendar-05, badge, sheet, tabs, avatar, tooltip, progress)?",
1539
1584
  true
1540
1585
  );
1541
1586
  if (addShadcn) {
@@ -1563,6 +1608,7 @@ if (args[0] === "init") {
1563
1608
  "tabs",
1564
1609
  "avatar",
1565
1610
  "tooltip",
1611
+ "progress",
1566
1612
  ]);
1567
1613
  });
1568
1614
  } else {
@@ -1654,6 +1700,20 @@ if (args[0] === "init") {
1654
1700
  console.log("- " + oscHyperlink("X", "https://x.com/TrustlessWork"));
1655
1701
  } else if (args[0] === "add" && args[1]) {
1656
1702
  const flags = parseFlags(args.slice(2));
1703
+ // Normalize common aliases (singular/plural, shorthand)
1704
+ const normalizeTemplateName = (name) => {
1705
+ let n = String(name).trim();
1706
+ // singular to plural base
1707
+ n = n.replace(/^escrow\b/, "escrows");
1708
+ n = n.replace(/^indicator\b/, "indicators");
1709
+ // allow nested segments singulars
1710
+ n = n.replace(/(^|\/)escrow(\/|$)/g, "$1escrows$2");
1711
+ n = n.replace(/(^|\/)indicator(\/|$)/g, "$1indicators$2");
1712
+ // friendly shape variants
1713
+ n = n.replace(/(^|\/)circle(\/|$)/g, "$1circular$2");
1714
+ return n;
1715
+ };
1716
+ args[1] = normalizeTemplateName(args[1]);
1657
1717
  const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
1658
1718
  if (!fs.existsSync(cfgPath)) {
1659
1719
  console.error(
@@ -1746,6 +1806,11 @@ if (args[0] === "init") {
1746
1806
 
1747
1807
  --- Escrows ---
1748
1808
  trustless-work add escrows
1809
+
1810
+ --- Indicators ---
1811
+ trustless-work add escrows/indicators/balance-progress
1812
+ trustless-work add escrows/indicators/balance-progress/bar
1813
+ trustless-work add escrows/indicators/balance-progress/donut
1749
1814
 
1750
1815
  --- Escrows by role ---
1751
1816
  trustless-work add escrows/escrows-by-role
@@ -1804,6 +1869,12 @@ if (args[0] === "init") {
1804
1869
  - trustless-work add escrows/multi-release/update-escrow
1805
1870
  - trustless-work add escrows/multi-release/update-escrow/form
1806
1871
  - trustless-work add escrows/multi-release/update-escrow/dialog
1872
+
1873
+ --- Withdraw remaining funds ---
1874
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds
1875
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/form
1876
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/button
1877
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/dialog
1807
1878
 
1808
1879
  --- Release escrow ---
1809
1880
  - trustless-work add escrows/multi-release/release-milestone
@@ -1812,6 +1883,12 @@ if (args[0] === "init") {
1812
1883
  --- Dispute escrow ---
1813
1884
  - trustless-work add escrows/multi-release/dispute-milestone
1814
1885
  - trustless-work add escrows/multi-release/dispute-milestone/button
1886
+
1887
+ --- Withdraw remaining funds ---
1888
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds
1889
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/form
1890
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/button
1891
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/dialog
1815
1892
 
1816
1893
  ----------------------
1817
1894
  --- SINGLE-MULTI-RELEASE -> Works with both types of escrows ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustless-work/blocks",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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": "^3.0.0",
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">
@@ -138,6 +155,9 @@ export const Actions = ({
138
155
  {/* Works only with single-release escrows */}
139
156
  {/* Only appears if all the milestones are approved */}
140
157
  {/* {shouldShowReleaseFundsButton && <ReleaseEscrowButton />} */}
158
+
159
+ {/* Multi-release: Withdraw Remaining Funds */}
160
+ {/* {shouldShowWithdrawRemaining && <WithdrawRemainingFundsDialog />} */}
141
161
  </div>
142
162
  )}
143
163
 
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import { Progress } from "__UI_BASE__/progress";
3
+ import { useGetMultipleEscrowBalancesQuery } from "@/components/tw-blocks/tanstack/useGetMultipleEscrowBalances";
4
+ import { formatCurrency } from "@/components/tw-blocks/helpers/format.helper";
5
+
6
+ type BalanceProgressBarProps = {
7
+ contractId: string;
8
+ target: number;
9
+ currency: string;
10
+ };
11
+
12
+ export const BalanceProgressBar = ({
13
+ contractId,
14
+ target,
15
+ currency,
16
+ }: BalanceProgressBarProps) => {
17
+ const isContractProvided = Boolean(
18
+ contractId && contractId.trim().length > 0
19
+ );
20
+
21
+ const { data, isLoading, isError } = useGetMultipleEscrowBalancesQuery({
22
+ addresses: isContractProvided ? [contractId] : [],
23
+ enabled: isContractProvided,
24
+ });
25
+
26
+ const currentBalanceRaw = Number(data?.[0]?.balance ?? 0);
27
+ const safeTarget = Number.isFinite(target) && target > 0 ? target : 0;
28
+ const progressValue =
29
+ safeTarget > 0
30
+ ? Math.min(100, Math.max(0, (currentBalanceRaw / safeTarget) * 100))
31
+ : 0;
32
+
33
+ return (
34
+ <div className="w-full">
35
+ <div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
36
+ <p>
37
+ <span className="font-bold mr-1">Balance:</span>
38
+ {isLoading
39
+ ? "Loading…"
40
+ : isError
41
+ ? "-"
42
+ : formatCurrency(currentBalanceRaw, currency)}
43
+ </p>
44
+ <p>
45
+ <span className="font-bold mr-1">Target:</span>{" "}
46
+ {formatCurrency(safeTarget, currency)}
47
+ </p>
48
+ </div>
49
+ <Progress
50
+ value={isLoading || isError ? 0 : progressValue}
51
+ className="w-full"
52
+ />
53
+ </div>
54
+ );
55
+ };
@@ -0,0 +1,99 @@
1
+ // @ts-nocheck
2
+ import * as React from "react";
3
+ import { useGetMultipleEscrowBalancesQuery } from "@/components/tw-blocks/tanstack/useGetMultipleEscrowBalances";
4
+ import { formatCurrency } from "@/components/tw-blocks/helpers/format.helper";
5
+
6
+ type BalanceProgressDonutProps = {
7
+ contractId: string;
8
+ target: number;
9
+ currency: string;
10
+ };
11
+
12
+ export const BalanceProgressDonut = ({
13
+ contractId,
14
+ target,
15
+ currency,
16
+ }: BalanceProgressDonutProps) => {
17
+ const isContractProvided = Boolean(
18
+ contractId && contractId.trim().length > 0
19
+ );
20
+
21
+ const { data, isLoading, isError } = useGetMultipleEscrowBalancesQuery({
22
+ addresses: isContractProvided ? [contractId] : [],
23
+ enabled: isContractProvided,
24
+ });
25
+
26
+ const currentBalanceRaw = Number(data?.[0]?.balance ?? 0);
27
+ const safeTarget = Number.isFinite(target) && target > 0 ? target : 0;
28
+ const progressValue =
29
+ safeTarget > 0
30
+ ? Math.min(100, Math.max(0, (currentBalanceRaw / safeTarget) * 100))
31
+ : 0;
32
+
33
+ return (
34
+ <div className="w-full">
35
+ <div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
36
+ <p>
37
+ <span className="font-bold mr-1">Balance:</span>
38
+ {isLoading
39
+ ? "Loading…"
40
+ : isError
41
+ ? "-"
42
+ : formatCurrency(currentBalanceRaw, currency)}
43
+ </p>
44
+ <p>
45
+ <span className="font-bold mr-1">Target:</span>{" "}
46
+ {formatCurrency(safeTarget, currency)}
47
+ </p>
48
+ </div>
49
+ {(() => {
50
+ const size = 160; // px
51
+ const stroke = 12; // px
52
+ const radius = (size - stroke) / 2;
53
+ const circumference = 2 * Math.PI * radius;
54
+ const pct = isLoading || isError ? 0 : progressValue;
55
+ const dashOffset = circumference * (1 - pct / 100);
56
+
57
+ return (
58
+ <div className="flex justify-center">
59
+ <div className="relative" style={{ width: size, height: size }}>
60
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
61
+ {/* Track */}
62
+ <circle
63
+ cx={size / 2}
64
+ cy={size / 2}
65
+ r={radius}
66
+ strokeWidth={stroke}
67
+ stroke="currentColor"
68
+ className="text-muted-foreground/20"
69
+ fill="none"
70
+ strokeLinecap="round"
71
+ />
72
+ {/* Progress */}
73
+ <g transform={`rotate(-90 ${size / 2} ${size / 2})`}>
74
+ <circle
75
+ cx={size / 2}
76
+ cy={size / 2}
77
+ r={radius}
78
+ strokeWidth={stroke}
79
+ stroke="currentColor"
80
+ className="text-primary"
81
+ fill="none"
82
+ strokeDasharray={`${circumference} ${circumference}`}
83
+ strokeDashoffset={dashOffset}
84
+ strokeLinecap="round"
85
+ />
86
+ </g>
87
+ </svg>
88
+
89
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
90
+ <span className="text-2xl font-bold">{Math.round(pct)}%</span>
91
+ <span className="text-muted-foreground text-sm">Progress</span>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ })()}
97
+ </div>
98
+ );
99
+ };
@@ -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 || "",
@@ -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>