@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,320 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { startOfDay, endOfDay, format } from "date-fns";
5
+ import type { DateRange as DayPickerDateRange } from "react-day-picker";
6
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
7
+ import type { SortingState } from "@tanstack/react-table";
8
+ import { useWalletContext } from "../../wallet-kit/WalletProvider";
9
+ import { useEscrowsBySignerQuery } from "../../tanstak/useEscrowsBySignerQuery";
10
+ import { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types";
11
+
12
+ export type EscrowOrderBy = "createdAt" | "updatedAt" | "amount";
13
+ export type EscrowOrderDirection = "asc" | "desc";
14
+ export type EscrowType = "single-release" | "multi-release" | "all";
15
+ export type EscrowStatus =
16
+ | "working"
17
+ | "pendingRelease"
18
+ | "released"
19
+ | "resolved"
20
+ | "inDispute"
21
+ | "all";
22
+ export type DateRange = DayPickerDateRange;
23
+
24
+ export function useEscrowsBySigner() {
25
+ const { walletAddress } = useWalletContext();
26
+ const router = useRouter();
27
+ const pathname = usePathname();
28
+ const searchParams = useSearchParams();
29
+
30
+ const [page, setPage] = React.useState<number>(1);
31
+ const [orderBy, setOrderBy] = React.useState<EscrowOrderBy>("createdAt");
32
+ const [orderDirection, setOrderDirection] =
33
+ React.useState<EscrowOrderDirection>("desc");
34
+ const [sorting, setSorting] = React.useState<SortingState>([]);
35
+ const [title, setTitle] = React.useState<string>("");
36
+ const [engagementId, setEngagementId] = React.useState<string>("");
37
+ const [isActive, setIsActive] = React.useState<boolean>(true);
38
+ const [validateOnChain, setValidateOnChain] = React.useState<boolean>(true);
39
+ const [type, setType] = React.useState<EscrowType>("all");
40
+ const [status, setStatus] = React.useState<EscrowStatus>("all");
41
+ const [minAmount, setMinAmount] = React.useState<string>("");
42
+ const [maxAmount, setMaxAmount] = React.useState<string>("");
43
+ const [dateRange, setDateRange] = React.useState<DateRange>({
44
+ from: undefined,
45
+ to: undefined,
46
+ });
47
+
48
+ function useDebouncedValue<T>(value: T, delayMs: number) {
49
+ const [debounced, setDebounced] = React.useState<T>(value);
50
+ React.useEffect(() => {
51
+ const id = setTimeout(() => setDebounced(value), delayMs);
52
+ return () => clearTimeout(id);
53
+ }, [value, delayMs]);
54
+ return debounced;
55
+ }
56
+
57
+ const debouncedTitle = useDebouncedValue(title, 400);
58
+ const debouncedEngagementId = useDebouncedValue(engagementId, 400);
59
+ const debouncedMinAmount = useDebouncedValue(minAmount, 400);
60
+ const debouncedMaxAmount = useDebouncedValue(maxAmount, 400);
61
+
62
+ React.useEffect(() => {
63
+ if (!searchParams) return;
64
+ const qp = new URLSearchParams(searchParams.toString());
65
+ const qpPage = Number(qp.get("page") || 1);
66
+ const qpOrderBy = (qp.get("orderBy") as EscrowOrderBy) || "createdAt";
67
+ const qpOrderDir =
68
+ (qp.get("orderDirection") as EscrowOrderDirection) || "desc";
69
+ const qpTitle = qp.get("title") || "";
70
+ const qpEng = qp.get("engagementId") || "";
71
+ const qpActive = qp.get("isActive");
72
+ const qpValidateOnChain = qp.get("validateOnChain");
73
+ const qpType = (qp.get("type") as EscrowType) || "all";
74
+ const qpStatus = (qp.get("status") as EscrowStatus) || "all";
75
+ const qpMin = qp.get("minAmount") || "";
76
+ const qpMax = qp.get("maxAmount") || "";
77
+ const qpStart = qp.get("startDate");
78
+ const qpEnd = qp.get("endDate");
79
+
80
+ setPage(Number.isFinite(qpPage) && qpPage > 0 ? qpPage : 1);
81
+ setOrderBy(
82
+ ["createdAt", "updatedAt", "amount"].includes(qpOrderBy)
83
+ ? qpOrderBy
84
+ : "createdAt"
85
+ );
86
+ setOrderDirection(qpOrderDir === "asc" ? "asc" : "desc");
87
+ setTitle(qpTitle);
88
+ setEngagementId(qpEng);
89
+ setIsActive(qpActive === null ? true : qpActive === "true");
90
+ setValidateOnChain(
91
+ qpValidateOnChain === null ? true : qpValidateOnChain === "true"
92
+ );
93
+ setType(qpType);
94
+ setStatus(qpStatus);
95
+ setMinAmount(qpMin);
96
+ setMaxAmount(qpMax);
97
+ setDateRange({
98
+ from: qpStart ? new Date(qpStart) : undefined,
99
+ to: qpEnd ? new Date(qpEnd) : undefined,
100
+ });
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
+ }, []);
103
+
104
+ const debouncedSearchParams = useDebouncedValue(
105
+ {
106
+ page,
107
+ orderBy,
108
+ orderDirection,
109
+ title: debouncedTitle,
110
+ engagementId: debouncedEngagementId,
111
+ isActive,
112
+ validateOnChain,
113
+ type,
114
+ status,
115
+ minAmount: debouncedMinAmount,
116
+ maxAmount: debouncedMaxAmount,
117
+ startDate: dateRange.from
118
+ ? startOfDay(dateRange.from).toISOString()
119
+ : undefined,
120
+ endDate: dateRange.to ? endOfDay(dateRange.to).toISOString() : undefined,
121
+ },
122
+ 200
123
+ );
124
+
125
+ React.useEffect(() => {
126
+ if (!pathname) return;
127
+ const qp = new URLSearchParams();
128
+ qp.set("page", String(debouncedSearchParams.page ?? 1));
129
+ qp.set("orderBy", String(debouncedSearchParams.orderBy ?? "createdAt"));
130
+ qp.set(
131
+ "orderDirection",
132
+ String(debouncedSearchParams.orderDirection ?? "desc")
133
+ );
134
+ if (debouncedSearchParams.title)
135
+ qp.set("title", debouncedSearchParams.title);
136
+ if (debouncedSearchParams.engagementId)
137
+ qp.set("engagementId", debouncedSearchParams.engagementId);
138
+ qp.set("isActive", String(debouncedSearchParams.isActive));
139
+ qp.set("validateOnChain", String(debouncedSearchParams.validateOnChain));
140
+ if (type && type !== "all") qp.set("type", type);
141
+ if (status && status !== "all") qp.set("status", status);
142
+ if (debouncedSearchParams.minAmount)
143
+ qp.set("minAmount", String(debouncedSearchParams.minAmount));
144
+ if (debouncedSearchParams.maxAmount)
145
+ qp.set("maxAmount", String(debouncedSearchParams.maxAmount));
146
+ if (debouncedSearchParams.startDate)
147
+ qp.set("startDate", String(debouncedSearchParams.startDate));
148
+ if (debouncedSearchParams.endDate)
149
+ qp.set("endDate", String(debouncedSearchParams.endDate));
150
+
151
+ router.replace(`${pathname}?${qp.toString()}`);
152
+ }, [
153
+ pathname,
154
+ router,
155
+ debouncedSearchParams.page,
156
+ debouncedSearchParams.orderBy,
157
+ debouncedSearchParams.orderDirection,
158
+ debouncedSearchParams.title,
159
+ debouncedSearchParams.engagementId,
160
+ debouncedSearchParams.isActive,
161
+ debouncedSearchParams.validateOnChain,
162
+ type,
163
+ status,
164
+ debouncedSearchParams.minAmount,
165
+ debouncedSearchParams.maxAmount,
166
+ debouncedSearchParams.startDate,
167
+ debouncedSearchParams.endDate,
168
+ ]);
169
+
170
+ const formattedRangeLabel = React.useMemo(() => {
171
+ if (!dateRange?.from && !dateRange?.to) return "Date range";
172
+ const fromStr = dateRange.from
173
+ ? format(dateRange.from, "LLL dd, yyyy")
174
+ : "";
175
+ const toStr = dateRange.to ? format(dateRange.to, "LLL dd, yyyy") : "";
176
+ return [fromStr, toStr].filter(Boolean).join(" – ") || "Date range";
177
+ }, [dateRange]);
178
+
179
+ const params = React.useMemo(() => {
180
+ return {
181
+ signer: walletAddress ?? "",
182
+ page,
183
+ orderBy,
184
+ orderDirection,
185
+ title: debouncedTitle || undefined,
186
+ engagementId: debouncedEngagementId || undefined,
187
+ isActive,
188
+ validateOnChain,
189
+ type: (type === "all" ? undefined : type) as
190
+ | undefined
191
+ | "single-release"
192
+ | "multi-release",
193
+ status: (status === "all" ? undefined : status) as
194
+ | undefined
195
+ | "working"
196
+ | "pendingRelease"
197
+ | "released"
198
+ | "resolved"
199
+ | "inDispute",
200
+ minAmount: debouncedMinAmount ? Number(debouncedMinAmount) : undefined,
201
+ maxAmount: debouncedMaxAmount ? Number(debouncedMaxAmount) : undefined,
202
+ startDate: dateRange.from
203
+ ? startOfDay(dateRange.from).toISOString()
204
+ : undefined,
205
+ endDate: dateRange.to ? endOfDay(dateRange.to).toISOString() : undefined,
206
+ enabled: Boolean(walletAddress),
207
+ };
208
+ }, [
209
+ walletAddress,
210
+ page,
211
+ orderBy,
212
+ orderDirection,
213
+ debouncedTitle,
214
+ debouncedEngagementId,
215
+ isActive,
216
+ validateOnChain,
217
+ type,
218
+ status,
219
+ debouncedMinAmount,
220
+ debouncedMaxAmount,
221
+ dateRange,
222
+ ]);
223
+
224
+ const query = useEscrowsBySignerQuery(params);
225
+ const nextPageQuery = useEscrowsBySignerQuery({ ...params, page: page + 1 });
226
+
227
+ const didMountValidateRef = React.useRef(false);
228
+ React.useEffect(() => {
229
+ if (!didMountValidateRef.current) {
230
+ didMountValidateRef.current = true;
231
+ return;
232
+ }
233
+ query.refetch();
234
+ nextPageQuery.refetch();
235
+ // eslint-disable-next-line react-hooks/exhaustive-deps
236
+ }, [validateOnChain]);
237
+
238
+ const onClearFilters = React.useCallback(() => {
239
+ setTitle("");
240
+ setEngagementId("");
241
+ setIsActive(true);
242
+ setValidateOnChain(true);
243
+ setType("all");
244
+ setStatus("all");
245
+ setMinAmount("");
246
+ setMaxAmount("");
247
+ setDateRange({ from: undefined, to: undefined });
248
+ setPage(1);
249
+ setOrderBy("createdAt");
250
+ setOrderDirection("desc");
251
+ setSorting([]);
252
+ }, []);
253
+
254
+ const handleSortingChange = React.useCallback(
255
+ (updater: SortingState | ((old: SortingState) => SortingState)) => {
256
+ setSorting((prev) => {
257
+ const next =
258
+ typeof updater === "function"
259
+ ? (updater as (old: SortingState) => SortingState)(prev)
260
+ : updater;
261
+ const first = next[0];
262
+ if (first) {
263
+ if (
264
+ first.id === "amount" ||
265
+ first.id === "createdAt" ||
266
+ first.id === "updatedAt"
267
+ ) {
268
+ setOrderBy(first.id as EscrowOrderBy);
269
+ setOrderDirection(first.desc ? "desc" : "asc");
270
+ }
271
+ } else {
272
+ setOrderBy("createdAt");
273
+ setOrderDirection("desc");
274
+ }
275
+ return next;
276
+ });
277
+ },
278
+ []
279
+ );
280
+
281
+ return {
282
+ walletAddress,
283
+ data: query.data ?? ([] as Escrow[]),
284
+ isLoading: query.isLoading,
285
+ isError: query.isError,
286
+ isFetching: query.isFetching,
287
+ refetch: query.refetch,
288
+ nextData: nextPageQuery.data ?? [],
289
+ isFetchingNext: nextPageQuery.isFetching,
290
+ page,
291
+ setPage,
292
+ orderBy,
293
+ setOrderBy,
294
+ orderDirection,
295
+ setOrderDirection,
296
+ sorting,
297
+ setSorting,
298
+ title,
299
+ setTitle,
300
+ engagementId,
301
+ setEngagementId,
302
+ isActive,
303
+ setIsActive,
304
+ validateOnChain,
305
+ setValidateOnChain,
306
+ type,
307
+ setType,
308
+ status,
309
+ setStatus,
310
+ minAmount,
311
+ setMinAmount,
312
+ maxAmount,
313
+ setMaxAmount,
314
+ dateRange,
315
+ setDateRange,
316
+ formattedRangeLabel,
317
+ onClearFilters,
318
+ handleSortingChange,
319
+ } as const;
320
+ }
@@ -0,0 +1,78 @@
1
+ import * as React from "react";
2
+ import { Button } from "__UI_BASE__/button";
3
+ import { useEscrowsMutations } from "@/components/tw-blocks/tanstak/useEscrowsMutations";
4
+ import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
5
+ import { ApproveMilestonePayload } from "@trustless-work/escrow/types";
6
+ import { toast } from "sonner";
7
+ import {
8
+ ErrorResponse,
9
+ handleError,
10
+ } from "@/components/tw-blocks/handle-errors/handle";
11
+ import { useEscrowContext } from "../../../escrow-context/EscrowProvider";
12
+ import { Loader2 } from "lucide-react";
13
+
14
+ type ApproveMilestoneButtonProps = {
15
+ milestoneIndex: number | string;
16
+ };
17
+
18
+ export default function ApproveMilestoneButton({
19
+ milestoneIndex,
20
+ }: ApproveMilestoneButtonProps) {
21
+ const { approveMilestone } = useEscrowsMutations();
22
+ const { selectedEscrow, updateEscrow } = useEscrowContext();
23
+ const { walletAddress } = useWalletContext();
24
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
25
+
26
+ async function handleClick() {
27
+ try {
28
+ setIsSubmitting(true);
29
+
30
+ const payload: ApproveMilestonePayload = {
31
+ contractId: selectedEscrow?.contractId || "",
32
+ milestoneIndex: String(milestoneIndex),
33
+ approver: walletAddress || "",
34
+ newFlag: true,
35
+ };
36
+
37
+ await approveMilestone.mutateAsync({
38
+ payload,
39
+ type: "single-release",
40
+ address: walletAddress || "",
41
+ });
42
+
43
+ toast.success("Milestone approved flag updated successfully");
44
+
45
+ updateEscrow({
46
+ ...selectedEscrow,
47
+ milestones: selectedEscrow?.milestones.map((milestone, index) => {
48
+ if (index === Number(milestoneIndex)) {
49
+ return { ...milestone, approved: true };
50
+ }
51
+ return milestone;
52
+ }),
53
+ });
54
+ } catch (error) {
55
+ toast.error(handleError(error as ErrorResponse).message);
56
+ } finally {
57
+ setIsSubmitting(false);
58
+ }
59
+ }
60
+
61
+ return (
62
+ <Button
63
+ type="button"
64
+ disabled={isSubmitting}
65
+ onClick={handleClick}
66
+ className="cursor-pointer w-full"
67
+ >
68
+ {isSubmitting ? (
69
+ <div className="flex items-center">
70
+ <Loader2 className="h-5 w-5 animate-spin" />
71
+ <span className="ml-2">Approving...</span>
72
+ </div>
73
+ ) : (
74
+ "Approve Milestone"
75
+ )}
76
+ </Button>
77
+ );
78
+ }
@@ -0,0 +1,102 @@
1
+ import * as React from "react";
2
+ import {
3
+ Form,
4
+ FormField,
5
+ FormItem,
6
+ FormLabel,
7
+ FormControl,
8
+ FormMessage,
9
+ } from "__UI_BASE__/form";
10
+ import { Button } from "__UI_BASE__/button";
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ DialogTrigger,
17
+ } from "__UI_BASE__/dialog";
18
+ import { Loader2 } from "lucide-react";
19
+ import { useApproveMilestone } from "./useApproveMilestone";
20
+ import { useEscrowContext } from "../../../escrow-context/EscrowProvider";
21
+ import {
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from "__UI_BASE__/select";
28
+
29
+ export default function ApproveMilestoneDialog() {
30
+ const { form, handleSubmit, isSubmitting } = useApproveMilestone();
31
+ const { selectedEscrow } = useEscrowContext();
32
+
33
+ return (
34
+ <Dialog>
35
+ <DialogTrigger asChild>
36
+ <Button type="button" className="cursor-pointer w-full">
37
+ Approve Milestone
38
+ </Button>
39
+ </DialogTrigger>
40
+ <DialogContent>
41
+ <DialogHeader>
42
+ <DialogTitle>Approve Milestone</DialogTitle>
43
+ </DialogHeader>
44
+ <Form {...form}>
45
+ <form
46
+ onSubmit={handleSubmit}
47
+ className="flex flex-col space-y-6 w-full"
48
+ >
49
+ <FormField
50
+ control={form.control}
51
+ name="milestoneIndex"
52
+ render={({ field }) => (
53
+ <FormItem>
54
+ <FormLabel className="flex items-center">
55
+ Milestone<span className="text-destructive ml-1">*</span>
56
+ </FormLabel>
57
+ <FormControl>
58
+ <Select
59
+ value={field.value}
60
+ onValueChange={(e) => {
61
+ field.onChange(e);
62
+ }}
63
+ >
64
+ <SelectTrigger className="w-full">
65
+ <SelectValue placeholder="Select milestone" />
66
+ </SelectTrigger>
67
+ <SelectContent>
68
+ {(selectedEscrow?.milestones || []).map((m, idx) => (
69
+ <SelectItem key={`ms-${idx}`} value={String(idx)}>
70
+ {m?.description || `Milestone ${idx + 1}`}
71
+ </SelectItem>
72
+ ))}
73
+ </SelectContent>
74
+ </Select>
75
+ </FormControl>
76
+ <FormMessage />
77
+ </FormItem>
78
+ )}
79
+ />
80
+
81
+ <div className="mt-4">
82
+ <Button
83
+ type="submit"
84
+ disabled={isSubmitting}
85
+ className="cursor-pointer"
86
+ >
87
+ {isSubmitting ? (
88
+ <div className="flex items-center">
89
+ <Loader2 className="h-5 w-5 animate-spin" />
90
+ <span className="ml-2">Approving...</span>
91
+ </div>
92
+ ) : (
93
+ "Approve"
94
+ )}
95
+ </Button>
96
+ </div>
97
+ </form>
98
+ </Form>
99
+ </DialogContent>
100
+ </Dialog>
101
+ );
102
+ }
@@ -0,0 +1,80 @@
1
+ import * as React from "react";
2
+ import {
3
+ Form,
4
+ FormField,
5
+ FormItem,
6
+ FormLabel,
7
+ FormControl,
8
+ FormMessage,
9
+ } from "__UI_BASE__/form";
10
+ import { Button } from "__UI_BASE__/button";
11
+ import { useApproveMilestone } from "./useApproveMilestone";
12
+ import { Loader2 } from "lucide-react";
13
+ import { useEscrowContext } from "../../../escrow-context/EscrowProvider";
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from "__UI_BASE__/select";
21
+
22
+ export default function ApproveMilestoneForm() {
23
+ const { form, handleSubmit, isSubmitting } = useApproveMilestone();
24
+ const { selectedEscrow } = useEscrowContext();
25
+
26
+ return (
27
+ <Form {...form}>
28
+ <form onSubmit={handleSubmit} className="flex flex-col space-y-6 w-full">
29
+ <FormField
30
+ control={form.control}
31
+ name="milestoneIndex"
32
+ render={({ field }) => (
33
+ <FormItem>
34
+ <FormLabel className="flex items-center">
35
+ Milestone<span className="text-destructive ml-1">*</span>
36
+ </FormLabel>
37
+ <FormControl>
38
+ <Select
39
+ value={field.value}
40
+ onValueChange={(e) => {
41
+ field.onChange(e);
42
+ }}
43
+ >
44
+ <SelectTrigger className="w-full">
45
+ <SelectValue placeholder="Select milestone" />
46
+ </SelectTrigger>
47
+ <SelectContent>
48
+ {(selectedEscrow?.milestones || []).map((m, idx) => (
49
+ <SelectItem key={`ms-${idx}`} value={String(idx)}>
50
+ {m?.description || `Milestone ${idx + 1}`}
51
+ </SelectItem>
52
+ ))}
53
+ </SelectContent>
54
+ </Select>
55
+ </FormControl>
56
+ <FormMessage />
57
+ </FormItem>
58
+ )}
59
+ />
60
+
61
+ <div className="mt-4">
62
+ <Button
63
+ type="submit"
64
+ disabled={isSubmitting}
65
+ className="cursor-pointer"
66
+ >
67
+ {isSubmitting ? (
68
+ <div className="flex items-center">
69
+ <Loader2 className="h-5 w-5 animate-spin" />
70
+ <span className="ml-2">Approving...</span>
71
+ </div>
72
+ ) : (
73
+ "Approve"
74
+ )}
75
+ </Button>
76
+ </div>
77
+ </form>
78
+ </Form>
79
+ );
80
+ }
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ export const approveMilestoneSchema = z.object({
4
+ milestoneIndex: z
5
+ .string({ required_error: "Milestone is required" })
6
+ .min(1, { message: "Milestone is required" }),
7
+ });
8
+
9
+ export type ApproveMilestoneValues = z.infer<typeof approveMilestoneSchema>;
@@ -0,0 +1,67 @@
1
+ import * as React from "react";
2
+ import { useForm } from "react-hook-form";
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { approveMilestoneSchema, type ApproveMilestoneValues } from "./schema";
5
+ import { toast } from "sonner";
6
+ import { ApproveMilestonePayload } from "@trustless-work/escrow";
7
+ import { useEscrowContext } from "../../../escrow-context/EscrowProvider";
8
+ import { useEscrowsMutations } from "@/components/tw-blocks/tanstak/useEscrowsMutations";
9
+ import {
10
+ ErrorResponse,
11
+ handleError,
12
+ } from "@/components/tw-blocks/handle-errors/handle";
13
+ import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
14
+
15
+ export function useApproveMilestone() {
16
+ const { approveMilestone } = useEscrowsMutations();
17
+ const { selectedEscrow, updateEscrow } = useEscrowContext();
18
+ const { walletAddress } = useWalletContext();
19
+
20
+ const form = useForm<ApproveMilestoneValues>({
21
+ resolver: zodResolver(approveMilestoneSchema),
22
+ defaultValues: {
23
+ milestoneIndex: "0",
24
+ },
25
+ mode: "onChange",
26
+ });
27
+
28
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
29
+
30
+ const handleSubmit = form.handleSubmit(async (payload) => {
31
+ try {
32
+ setIsSubmitting(true);
33
+
34
+ const finalPayload: ApproveMilestonePayload = {
35
+ contractId: selectedEscrow?.contractId || "",
36
+ milestoneIndex: payload.milestoneIndex,
37
+ approver: walletAddress || "",
38
+ newFlag: true,
39
+ };
40
+
41
+ await approveMilestone.mutateAsync({
42
+ payload: finalPayload,
43
+ type: "single-release",
44
+ address: walletAddress || "",
45
+ });
46
+
47
+ toast.success("Milestone approved flag updated successfully");
48
+
49
+ updateEscrow({
50
+ ...selectedEscrow,
51
+ milestones: selectedEscrow?.milestones.map((milestone, index) => {
52
+ if (index === Number(payload.milestoneIndex)) {
53
+ return { ...milestone, approved: true };
54
+ }
55
+ return milestone;
56
+ }),
57
+ });
58
+ } catch (error) {
59
+ toast.error(handleError(error as ErrorResponse).message);
60
+ } finally {
61
+ setIsSubmitting(false);
62
+ form.reset();
63
+ }
64
+ });
65
+
66
+ return { form, handleSubmit, isSubmitting };
67
+ }