@trustless-work/blocks 1.0.5 → 1.0.7

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 (34) hide show
  1. package/bin/index.js +1930 -1920
  2. package/package.json +1 -1
  3. package/templates/dashboard/dashboard-01/Dashboard.tsx +118 -71
  4. package/templates/deps.json +1 -1
  5. package/templates/escrows/details/Actions.tsx +1 -2
  6. package/templates/escrows/details/Entities.tsx +23 -2
  7. package/templates/escrows/details/GeneralInformation.tsx +1 -13
  8. package/templates/escrows/details/MilestoneCard.tsx +1 -1
  9. package/templates/escrows/details/MilestoneDetailDialog.tsx +38 -19
  10. package/templates/escrows/details/SuccessReleaseDialog.tsx +84 -28
  11. package/templates/escrows/details/useDetailsEscrow.ts +15 -2
  12. package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +76 -101
  13. package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +78 -102
  14. package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +8 -18
  15. package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +21 -12
  16. package/templates/escrows/multi-release/release-milestone/button/ReleaseMilestone.tsx +5 -1
  17. package/templates/escrows/multi-release/update-escrow/dialog/UpdateEscrow.tsx +112 -101
  18. package/templates/escrows/multi-release/update-escrow/form/UpdateEscrow.tsx +103 -101
  19. package/templates/escrows/multi-release/update-escrow/shared/schema.ts +8 -16
  20. package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +33 -14
  21. package/templates/escrows/multi-release/withdraw-remaining-funds/button/WithdrawRemainingFunds.tsx +0 -1
  22. package/templates/escrows/multi-release/withdraw-remaining-funds/shared/useWithdrawRemainingFunds.ts +0 -1
  23. package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +2 -25
  24. package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +3 -26
  25. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -10
  26. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -4
  27. package/templates/escrows/single-release/release-escrow/button/ReleaseEscrow.tsx +5 -1
  28. package/templates/escrows/single-release/update-escrow/dialog/UpdateEscrow.tsx +41 -27
  29. package/templates/escrows/single-release/update-escrow/form/UpdateEscrow.tsx +38 -3
  30. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +28 -14
  31. package/templates/providers/EscrowAmountProvider.tsx +8 -0
  32. package/templates/tanstack/useEscrowsMutations.ts +1 -6
  33. package/templates/wallet-kit/WalletButtons.tsx +2 -2
  34. package/templates/wallet-kit/WalletProvider.tsx +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustless-work/blocks",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "author": "Trustless Work",
5
5
  "keywords": [
6
6
  "react",
@@ -11,7 +11,7 @@ import {
11
11
  import { Separator } from "__UI_BASE__/separator";
12
12
  import { useDashboard } from "./useDashboard";
13
13
  import { formatCurrency } from "../../helpers/format.helper";
14
- import { Activity, Layers3, PiggyBank } from "lucide-react";
14
+ import { Activity, Layers3, PiggyBank, CloudOff } from "lucide-react";
15
15
  import {
16
16
  AreaChart,
17
17
  Area,
@@ -29,6 +29,13 @@ import {
29
29
  ChartTooltipContent,
30
30
  type ChartConfig,
31
31
  } from "__UI_BASE__/chart";
32
+ import {
33
+ Empty,
34
+ EmptyHeader,
35
+ EmptyMedia,
36
+ EmptyTitle,
37
+ EmptyDescription,
38
+ } from "__UI_BASE__/empty";
32
39
 
33
40
  const chartConfigBar: ChartConfig = {
34
41
  desktop: {
@@ -86,7 +93,7 @@ export const Dashboard01 = () => {
86
93
  <div className="grid gap-6">
87
94
  {/* KPI Cards */}
88
95
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
89
- <Card>
96
+ <Card className="gap-3">
90
97
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
91
98
  <CardTitle className="text-sm font-medium">Escrows</CardTitle>
92
99
  <Layers3 className="h-4 w-4 text-muted-foreground" />
@@ -101,7 +108,7 @@ export const Dashboard01 = () => {
101
108
  </CardContent>
102
109
  </Card>
103
110
 
104
- <Card>
111
+ <Card className="gap-3">
105
112
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
106
113
  <CardTitle className="text-sm font-medium">Total Amount</CardTitle>
107
114
  <Activity className="h-4 w-4 text-muted-foreground" />
@@ -116,7 +123,7 @@ export const Dashboard01 = () => {
116
123
  </CardContent>
117
124
  </Card>
118
125
 
119
- <Card>
126
+ <Card className="gap-3">
120
127
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
121
128
  <CardTitle className="text-sm font-medium">Total Balance</CardTitle>
122
129
  <PiggyBank className="h-4 w-4 text-muted-foreground" />
@@ -137,7 +144,7 @@ export const Dashboard01 = () => {
137
144
  {/* Charts */}
138
145
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
139
146
  {/* Bar chart: Amounts by date (shadcn pattern) */}
140
- <Card>
147
+ <Card className="gap-3">
141
148
  <CardHeader>
142
149
  <CardTitle>Escrow Amounts</CardTitle>
143
150
  <CardDescription>Amounts by date</CardDescription>
@@ -147,26 +154,42 @@ export const Dashboard01 = () => {
147
154
  className="w-full h-56 sm:h-64 lg:h-72"
148
155
  config={chartConfigBar}
149
156
  >
150
- <BarChart accessibilityLayer data={barData}>
151
- <CartesianGrid vertical={false} />
152
- <XAxis
153
- dataKey="month"
154
- tickLine={false}
155
- tickMargin={10}
156
- axisLine={false}
157
- tickFormatter={(value) =>
158
- new Date(String(value)).toLocaleDateString("en-US", {
159
- month: "short",
160
- day: "numeric",
161
- })
162
- }
163
- />
164
- <ChartTooltip
165
- cursor={false}
166
- content={<ChartTooltipContent hideLabel />}
167
- />
168
- <Bar dataKey="desktop" fill="var(--color-desktop)" radius={8} />
169
- </BarChart>
157
+ {barData.length > 0 ? (
158
+ <BarChart accessibilityLayer data={barData}>
159
+ <CartesianGrid vertical={false} />
160
+ <XAxis
161
+ dataKey="month"
162
+ tickLine={false}
163
+ tickMargin={10}
164
+ axisLine={false}
165
+ tickFormatter={(value) =>
166
+ new Date(String(value)).toLocaleDateString("en-US", {
167
+ month: "short",
168
+ day: "numeric",
169
+ })
170
+ }
171
+ />
172
+ <ChartTooltip
173
+ cursor={false}
174
+ content={<ChartTooltipContent hideLabel />}
175
+ />
176
+ <Bar
177
+ dataKey="desktop"
178
+ fill="var(--color-desktop)"
179
+ radius={8}
180
+ />
181
+ </BarChart>
182
+ ) : (
183
+ <Empty className="border border-dashed">
184
+ <EmptyHeader>
185
+ <EmptyMedia variant="icon">
186
+ <CloudOff />
187
+ </EmptyMedia>
188
+ <EmptyTitle>No data</EmptyTitle>
189
+ <EmptyDescription>No Data Available</EmptyDescription>
190
+ </EmptyHeader>
191
+ </Empty>
192
+ )}
170
193
  </ChartContainer>
171
194
  </CardContent>
172
195
  </Card>
@@ -182,22 +205,34 @@ export const Dashboard01 = () => {
182
205
  config={chartConfigDonut}
183
206
  className="w-full h-56 sm:h-64 lg:h-72"
184
207
  >
185
- <PieChart>
186
- <ChartTooltip
187
- cursor={false}
188
- content={<ChartTooltipContent hideLabel />}
189
- />
190
- <Pie
191
- data={donutData}
192
- dataKey="visitors"
193
- nameKey="browser"
194
- innerRadius={60}
195
- >
196
- {donutData.map((slice, idx) => (
197
- <Cell key={`cell-${idx}`} fill={slice.fill} />
198
- ))}
199
- </Pie>
200
- </PieChart>
208
+ {donutData.some((d) => Number(d.visitors) > 0) ? (
209
+ <PieChart>
210
+ <ChartTooltip
211
+ cursor={false}
212
+ content={<ChartTooltipContent hideLabel />}
213
+ />
214
+ <Pie
215
+ data={donutData}
216
+ dataKey="visitors"
217
+ nameKey="browser"
218
+ innerRadius={60}
219
+ >
220
+ {donutData.map((slice, idx) => (
221
+ <Cell key={`cell-${idx}`} fill={slice.fill} />
222
+ ))}
223
+ </Pie>
224
+ </PieChart>
225
+ ) : (
226
+ <Empty className="border border-dashed">
227
+ <EmptyHeader>
228
+ <EmptyMedia variant="icon">
229
+ <CloudOff />
230
+ </EmptyMedia>
231
+ <EmptyTitle>No data</EmptyTitle>
232
+ <EmptyDescription>No Data Available</EmptyDescription>
233
+ </EmptyHeader>
234
+ </Empty>
235
+ )}
201
236
  </ChartContainer>
202
237
  <div className="mt-4 flex items-center justify-center gap-6">
203
238
  <div className="flex items-center gap-2">
@@ -229,36 +264,48 @@ export const Dashboard01 = () => {
229
264
  className="w-full h-56 sm:h-64 lg:h-72"
230
265
  config={chartConfigArea}
231
266
  >
232
- <AreaChart
233
- accessibilityLayer
234
- data={areaData}
235
- margin={{ left: 12, right: 12 }}
236
- >
237
- <CartesianGrid vertical={false} />
238
- <XAxis
239
- dataKey="month"
240
- tickLine={false}
241
- axisLine={false}
242
- tickMargin={8}
243
- tickFormatter={(value) =>
244
- new Date(String(value)).toLocaleDateString("en-US", {
245
- month: "short",
246
- day: "numeric",
247
- })
248
- }
249
- />
250
- <ChartTooltip
251
- cursor={false}
252
- content={<ChartTooltipContent indicator="line" />}
253
- />
254
- <Area
255
- dataKey="desktop"
256
- type="natural"
257
- fill="var(--color-desktop)"
258
- fillOpacity={0.4}
259
- stroke="var(--color-desktop)"
260
- />
261
- </AreaChart>
267
+ {areaData.length > 0 ? (
268
+ <AreaChart
269
+ accessibilityLayer
270
+ data={areaData}
271
+ margin={{ left: 12, right: 12 }}
272
+ >
273
+ <CartesianGrid vertical={false} />
274
+ <XAxis
275
+ dataKey="month"
276
+ tickLine={false}
277
+ axisLine={false}
278
+ tickMargin={8}
279
+ tickFormatter={(value) =>
280
+ new Date(String(value)).toLocaleDateString("en-US", {
281
+ month: "short",
282
+ day: "numeric",
283
+ })
284
+ }
285
+ />
286
+ <ChartTooltip
287
+ cursor={false}
288
+ content={<ChartTooltipContent indicator="line" />}
289
+ />
290
+ <Area
291
+ dataKey="desktop"
292
+ type="natural"
293
+ fill="var(--color-desktop)"
294
+ fillOpacity={0.4}
295
+ stroke="var(--color-desktop)"
296
+ />
297
+ </AreaChart>
298
+ ) : (
299
+ <Empty className="border border-dashed">
300
+ <EmptyHeader>
301
+ <EmptyMedia variant="icon">
302
+ <CloudOff />
303
+ </EmptyMedia>
304
+ <EmptyTitle>No data</EmptyTitle>
305
+ <EmptyDescription>No Data Available</EmptyDescription>
306
+ </EmptyHeader>
307
+ </Empty>
308
+ )}
262
309
  </ChartContainer>
263
310
  </CardContent>
264
311
  </Card>
@@ -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": "^3.0.0",
7
+ "@trustless-work/escrow": "^3.0.2",
8
8
  "@tanstack/react-query": "^5.75.0",
9
9
  "@tanstack/react-query-devtools": "^5.75.0",
10
10
  "tailwindcss": "^3.3.3",
@@ -91,8 +91,7 @@ export const Actions = ({
91
91
  userRolesInEscrow.includes("platformAddress") &&
92
92
  !selectedEscrow?.flags?.disputed &&
93
93
  !selectedEscrow?.flags?.resolved &&
94
- !selectedEscrow?.flags?.released &&
95
- selectedEscrow?.balance === 0;
94
+ !selectedEscrow?.flags?.released;
96
95
 
97
96
  const shouldShowDisputeButton =
98
97
  selectedEscrow.type === "single-release" &&
@@ -1,13 +1,26 @@
1
1
  "use client";
2
2
 
3
- import { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types";
3
+ import {
4
+ GetEscrowsFromIndexerResponse as Escrow,
5
+ MultiReleaseMilestone,
6
+ } from "@trustless-work/escrow/types";
4
7
  import { EntityCard } from "./EntityCard";
8
+ import { Separator } from "__UI_BASE__/separator";
5
9
 
6
10
  interface EntitiesProps {
7
11
  selectedEscrow: Escrow;
8
12
  }
9
13
 
10
14
  export const Entities = ({ selectedEscrow }: EntitiesProps) => {
15
+ const receivers =
16
+ selectedEscrow.type === "single-release"
17
+ ? (selectedEscrow.roles as { receiver?: string })?.receiver
18
+ ? [(selectedEscrow.roles as { receiver?: string }).receiver as string]
19
+ : []
20
+ : (selectedEscrow.milestones || [])
21
+ .map((m) => (m as MultiReleaseMilestone | undefined)?.receiver)
22
+ .filter((r): r is string => Boolean(r));
23
+
11
24
  return (
12
25
  <>
13
26
  <div className="flex justify-between items-center mb-6">
@@ -41,7 +54,15 @@ export const Entities = ({ selectedEscrow }: EntitiesProps) => {
41
54
  type="releaseSigner"
42
55
  entity={selectedEscrow.roles?.releaseSigner}
43
56
  />
44
- <EntityCard type="receiver" entity={selectedEscrow.roles?.receiver} />
57
+ </div>
58
+
59
+ <Separator className="my-4" />
60
+
61
+ <h2 className="text-lg font-semibold">Receivers</h2>
62
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
63
+ {receivers.map((r, idx) => (
64
+ <EntityCard key={`receiver-${idx}-${r}`} type="receiver" entity={r} />
65
+ ))}
45
66
  </div>
46
67
  </>
47
68
  );
@@ -159,7 +159,7 @@ export const GeneralInformation = ({
159
159
  </div>
160
160
  </div>
161
161
 
162
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
162
+ <div className="grid grid-cols-1 gap-4">
163
163
  <div className="p-4 bg-muted/50 rounded-lg border">
164
164
  <div className="flex items-center gap-3 mb-3">
165
165
  <Users className="h-5 w-5 text-primary flex-shrink-0" />
@@ -185,18 +185,6 @@ export const GeneralInformation = ({
185
185
  })}
186
186
  </div>
187
187
  </div>
188
-
189
- <div className="p-4 bg-muted/50 rounded-lg border">
190
- <div className="flex items-center gap-3 mb-2">
191
- <Info className="h-5 w-5 text-primary flex-shrink-0" />
192
- <span className="text-sm font-medium text-muted-foreground">
193
- Memo
194
- </span>
195
- </div>
196
- <span className="font-medium text-foreground">
197
- {selectedEscrow?.receiverMemo || "No Memo"}
198
- </span>
199
- </div>
200
188
  </div>
201
189
 
202
190
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -221,7 +221,7 @@ const MilestoneCardComponent = ({
221
221
  <Button
222
222
  size="sm"
223
223
  variant="outline"
224
- className="w-full border-border text-muted-foreground"
224
+ className="w-full border-border text-muted-foreground cursor-pointer"
225
225
  onClick={(e) => {
226
226
  e.stopPropagation();
227
227
  onViewDetails(milestone, milestoneIndex);
@@ -10,7 +10,6 @@ import {
10
10
  import { Card, CardContent, CardHeader, CardTitle } from "__UI_BASE__/card";
11
11
  import { Badge } from "__UI_BASE__/badge";
12
12
  import {
13
- DollarSign,
14
13
  FileCheck2,
15
14
  User,
16
15
  Calendar,
@@ -28,6 +27,9 @@ import {
28
27
  SingleReleaseMilestone,
29
28
  } from "@trustless-work/escrow";
30
29
  import Link from "next/link";
30
+ import { formatCurrency } from "@/components/tw-blocks/helpers/format.helper";
31
+ import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
32
+ import { EntityCard } from "./EntityCard";
31
33
 
32
34
  interface MilestoneDetailDialogProps {
33
35
  isOpen: boolean;
@@ -49,6 +51,8 @@ export const MilestoneDetailDialog = ({
49
51
  onClose,
50
52
  selectedMilestone,
51
53
  }: MilestoneDetailDialogProps) => {
54
+ const { selectedEscrow } = useEscrowContext();
55
+
52
56
  const getMilestoneStatusBadge = (
53
57
  milestone: SingleReleaseMilestone | MultiReleaseMilestone
54
58
  ) => {
@@ -115,11 +119,15 @@ export const MilestoneDetailDialog = ({
115
119
  }
116
120
  };
117
121
 
122
+ const currentReceiver =
123
+ selectedEscrow?.type === "multi-release" &&
124
+ ((selectedMilestone?.milestone as MultiReleaseMilestone)?.receiver ?? "");
125
+
118
126
  if (!selectedMilestone) return null;
119
127
 
120
128
  return (
121
129
  <Dialog open={isOpen} onOpenChange={onClose}>
122
- <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
130
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
123
131
  <DialogHeader className="pb-4 border-b border-border">
124
132
  <div className="flex items-center gap-3">
125
133
  <div className="flex items-center justify-center w-10 h-10 bg-primary/10 rounded-full">
@@ -154,6 +162,28 @@ export const MilestoneDetailDialog = ({
154
162
  {getMilestoneStatusBadge(selectedMilestone.milestone)}
155
163
  </div>
156
164
 
165
+ {"amount" in selectedMilestone.milestone && (
166
+ <div className="flex items-center justify-between p-4 rounded-lg border border-border">
167
+ <div className="flex items-center gap-3">
168
+ <div className="flex items-center justify-center w-8 h-8 bg-background rounded-full shadow-sm border border-border">
169
+ <Calendar className="w-4 h-4 text-primary" />
170
+ </div>
171
+ <div>
172
+ <p className="text-sm font-medium text-foreground">Amount</p>
173
+ <p className="text-xs text-muted-foreground">
174
+ The amount of the milestone
175
+ </p>
176
+ </div>
177
+ </div>
178
+ <span className="font-bold text-foreground">
179
+ {formatCurrency(
180
+ selectedMilestone.milestone.amount,
181
+ selectedEscrow?.trustline?.name ?? "USDC"
182
+ )}
183
+ </span>
184
+ </div>
185
+ )}
186
+
157
187
  <Card className="border border-border shadow-sm">
158
188
  <CardHeader className="pb-3">
159
189
  <CardTitle className="text-lg flex items-center gap-2">
@@ -171,21 +201,6 @@ export const MilestoneDetailDialog = ({
171
201
  {selectedMilestone.milestone.description}
172
202
  </p>
173
203
  </div>
174
-
175
- {"amount" in selectedMilestone.milestone && (
176
- <div className="space-y-2">
177
- <label className="text-sm font-medium text-foreground flex items-center gap-2">
178
- <DollarSign className="w-4 h-4 text-green-600 dark:text-green-400" />
179
- Amount
180
- </label>
181
- <div className="flex items-center gap-2 bg-green-500/10 dark:bg-green-400/10 p-3 rounded-md border-l-4 border-green-500/20 dark:border-green-400/20">
182
- <DollarSign className="w-5 h-5 text-green-600 dark:text-green-400" />
183
- <span className="text-lg font-bold text-green-700 dark:text-green-300">
184
- {selectedMilestone.milestone.amount}
185
- </span>
186
- </div>
187
- </div>
188
- )}
189
204
  </CardContent>
190
205
  </Card>
191
206
 
@@ -193,7 +208,7 @@ export const MilestoneDetailDialog = ({
193
208
  <Card className="border border-border shadow-sm">
194
209
  <CardHeader className="pb-3">
195
210
  <CardTitle className="text-lg flex items-center gap-2">
196
- <div className="w-1 h-6 bg-green-600 dark:bg-green-400 rounded-full"></div>
211
+ <div className="w-1 h-6 rounded-full"></div>
197
212
  Evidence
198
213
  </CardTitle>
199
214
  </CardHeader>
@@ -203,7 +218,7 @@ export const MilestoneDetailDialog = ({
203
218
  <FileCheck2 className="w-4 h-4 text-green-600 dark:text-green-400" />
204
219
  Evidence URL
205
220
  </label>
206
- <div className="bg-muted/50 p-3 rounded-md border-l-4 border-green-500/20 dark:border-green-400/20">
221
+ <div className="bg-muted/50 p-3 rounded-md border-l-4">
207
222
  {(() => {
208
223
  const result = isValidUrl(
209
224
  selectedMilestone.milestone.evidence
@@ -269,6 +284,10 @@ export const MilestoneDetailDialog = ({
269
284
  </Card>
270
285
  )}
271
286
  </div>
287
+
288
+ {currentReceiver && (
289
+ <EntityCard type="receiver" entity={currentReceiver} />
290
+ )}
272
291
  </DialogContent>
273
292
  </Dialog>
274
293
  );
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import React, { useMemo } from "react";
3
+ import { useMemo } from "react";
4
4
  import {
5
5
  Dialog,
6
6
  DialogContent,
@@ -12,6 +12,7 @@ import { EntityCard } from "./EntityCard";
12
12
  import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
13
13
  import { useEscrowAmountContext } from "@/components/tw-blocks/providers/EscrowAmountProvider";
14
14
  import { CircleCheckBig } from "lucide-react";
15
+ import { MultiReleaseMilestone } from "@trustless-work/escrow";
15
16
 
16
17
  interface SuccessReleaseDialogProps {
17
18
  isOpen: boolean;
@@ -23,8 +24,12 @@ export const SuccessReleaseDialog = ({
23
24
  onOpenChange,
24
25
  }: SuccessReleaseDialogProps) => {
25
26
  const { selectedEscrow } = useEscrowContext();
26
- const { receiverAmount, platformFeeAmount, trustlessWorkAmount } =
27
- useEscrowAmountContext();
27
+ const {
28
+ receiverAmount,
29
+ platformFeeAmount,
30
+ trustlessWorkAmount,
31
+ lastReleasedMilestoneIndex,
32
+ } = useEscrowAmountContext();
28
33
 
29
34
  const platformFee = Number(selectedEscrow?.platformFee || 0);
30
35
  const trustlessPercentage = 0.3;
@@ -32,8 +37,15 @@ export const SuccessReleaseDialog = ({
32
37
 
33
38
  const currency = selectedEscrow?.trustline?.name ?? "";
34
39
 
35
- const cards = useMemo(
36
- () => [
40
+ const cards = useMemo<
41
+ Array<{
42
+ type: string;
43
+ entity?: string;
44
+ percentage?: number;
45
+ amount?: number;
46
+ }>
47
+ >(() => {
48
+ const baseCards = [
37
49
  {
38
50
  type: "Platform",
39
51
  entity: selectedEscrow?.roles?.platformAddress,
@@ -46,24 +58,62 @@ export const SuccessReleaseDialog = ({
46
58
  percentage: trustlessPercentage,
47
59
  amount: trustlessWorkAmount,
48
60
  },
49
- {
50
- type: "Receiver",
51
- entity: selectedEscrow?.roles?.receiver,
52
- percentage: receiverPercentage,
53
- amount: receiverAmount,
54
- },
55
- ],
56
- [
57
- platformFee,
58
- receiverPercentage,
59
- trustlessPercentage,
60
- platformFeeAmount,
61
- trustlessWorkAmount,
62
- receiverAmount,
63
- selectedEscrow?.roles?.platformAddress,
64
- selectedEscrow?.roles?.receiver,
65
- ]
66
- );
61
+ ];
62
+
63
+ if (selectedEscrow?.type === "single-release") {
64
+ return [
65
+ ...baseCards,
66
+ {
67
+ type: "Receiver",
68
+ entity: (selectedEscrow?.roles as { receiver?: string })?.receiver,
69
+ percentage: receiverPercentage,
70
+ amount: receiverAmount,
71
+ },
72
+ ];
73
+ }
74
+
75
+ // Multi-release: show only the receiver for the just-released milestone
76
+ const idx =
77
+ typeof lastReleasedMilestoneIndex === "number"
78
+ ? lastReleasedMilestoneIndex
79
+ : -1;
80
+ const milestone = selectedEscrow?.milestones?.[idx] as
81
+ | MultiReleaseMilestone
82
+ | undefined;
83
+ const receiverForReleased = milestone?.receiver;
84
+
85
+ if (receiverForReleased) {
86
+ return [
87
+ ...baseCards,
88
+ {
89
+ type: "Receiver",
90
+ entity: receiverForReleased,
91
+ percentage: receiverPercentage,
92
+ amount: receiverAmount,
93
+ },
94
+ ];
95
+ }
96
+
97
+ // Fallback: if no index available, list all receivers (legacy behavior)
98
+ const receiverCards = (selectedEscrow?.milestones || [])
99
+ .map((m) => (m as MultiReleaseMilestone | undefined)?.receiver)
100
+ .filter((r): r is string => Boolean(r))
101
+ .map((r) => ({ type: "Receiver", entity: r }));
102
+
103
+ return [...baseCards, ...receiverCards];
104
+ }, [
105
+ platformFee,
106
+ receiverPercentage,
107
+ trustlessPercentage,
108
+ platformFeeAmount,
109
+ trustlessWorkAmount,
110
+ receiverAmount,
111
+ selectedEscrow?.roles?.platformAddress,
112
+ selectedEscrow?.type,
113
+ selectedEscrow?.milestones,
114
+ selectedEscrow?.roles,
115
+ lastReleasedMilestoneIndex,
116
+ ]);
67
117
 
68
118
  return (
69
119
  <Dialog open={isOpen} onOpenChange={onOpenChange}>
@@ -81,13 +131,19 @@ export const SuccessReleaseDialog = ({
81
131
  <div className="flex flex-col gap-3">
82
132
  {cards.map((c) => (
83
133
  <EntityCard
84
- key={c.type}
134
+ key={`${c.type}-${c.entity ?? "unknown"}`}
85
135
  type={c.type}
86
136
  entity={c.entity}
87
- hasPercentage
88
- percentage={Number(c.percentage.toFixed(2))}
89
- hasAmount
90
- amount={Number(c.amount.toFixed(2))}
137
+ hasPercentage={c.percentage !== undefined}
138
+ percentage={
139
+ c.percentage !== undefined
140
+ ? Number(c.percentage.toFixed(2))
141
+ : undefined
142
+ }
143
+ hasAmount={c.amount !== undefined}
144
+ amount={
145
+ c.amount !== undefined ? Number(c.amount.toFixed(2)) : undefined
146
+ }
91
147
  currency={currency}
92
148
  />
93
149
  ))}
@@ -1,5 +1,8 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types";
2
+ import {
3
+ GetEscrowsFromIndexerResponse as Escrow,
4
+ MultiReleaseMilestone,
5
+ } from "@trustless-work/escrow/types";
3
6
  import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
4
7
  import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
5
8
  import { useEscrowAmountContext } from "@/components/tw-blocks/providers/EscrowAmountProvider";
@@ -60,7 +63,17 @@ const useEscrowDetailDialog = ({
60
63
  name: "disputeResolver",
61
64
  address: selectedEscrow.roles.disputeResolver,
62
65
  },
63
- { name: "receiver", address: selectedEscrow.roles.receiver },
66
+ {
67
+ name: "receiver",
68
+ address:
69
+ selectedEscrow.type === "single-release"
70
+ ? (selectedEscrow.roles as { receiver?: string })?.receiver
71
+ : (
72
+ selectedEscrow.milestones?.[0] as
73
+ | MultiReleaseMilestone
74
+ | undefined
75
+ )?.receiver,
76
+ },
64
77
  ];
65
78
 
66
79
  const userRoles = roleMappings