@trustless-work/blocks 0.0.7 → 0.0.8

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 (66) hide show
  1. package/bin/index.js +485 -17
  2. package/package.json +1 -1
  3. package/templates/escrows/details/Actions.tsx +144 -149
  4. package/templates/escrows/details/Entities.tsx +1 -1
  5. package/templates/escrows/details/EntityCard.tsx +1 -3
  6. package/templates/escrows/details/EscrowDetailDialog.tsx +16 -16
  7. package/templates/escrows/details/GeneralInformation.tsx +19 -22
  8. package/templates/escrows/details/MilestoneCard.tsx +46 -47
  9. package/templates/escrows/details/MilestoneDetailDialog.tsx +1 -2
  10. package/templates/escrows/details/Milestones.tsx +0 -5
  11. package/templates/escrows/details/SuccessReleaseDialog.tsx +4 -6
  12. package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +84 -49
  13. package/templates/escrows/escrows-by-role/cards/Filters.tsx +3 -5
  14. package/templates/escrows/escrows-by-role/table/EscrowsTable.tsx +8 -26
  15. package/templates/escrows/escrows-by-role/table/Filters.tsx +3 -5
  16. package/templates/escrows/escrows-by-signer/cards/EscrowsCards.tsx +89 -55
  17. package/templates/escrows/escrows-by-signer/cards/Filters.tsx +3 -5
  18. package/templates/escrows/escrows-by-signer/table/EscrowsTable.tsx +8 -24
  19. package/templates/escrows/escrows-by-signer/table/Filters.tsx +3 -5
  20. package/templates/escrows/multi-release/dispute-milestone/button/DisputeEscrow.tsx +98 -0
  21. package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +528 -0
  22. package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +506 -0
  23. package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +179 -0
  24. package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +175 -0
  25. package/templates/escrows/multi-release/release-milestone/button/ReleaseEscrow.tsx +116 -0
  26. package/templates/escrows/multi-release/resolve-dispute/button/ResolveDispute.tsx +122 -0
  27. package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +178 -0
  28. package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +156 -0
  29. package/templates/escrows/multi-release/resolve-dispute/shared/schema.ts +85 -0
  30. package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +105 -0
  31. package/templates/escrows/multi-release/update-escrow/dialog/UpdateEscrow.tsx +471 -0
  32. package/templates/escrows/multi-release/update-escrow/form/UpdateEscrow.tsx +449 -0
  33. package/templates/escrows/multi-release/update-escrow/shared/schema.ts +152 -0
  34. package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +254 -0
  35. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/button/ApproveMilestone.tsx +20 -7
  36. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/dialog/ApproveMilestone.tsx +3 -3
  37. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/form/ApproveMilestone.tsx +3 -3
  38. package/templates/escrows/{single-release/approve-milestone/shared → single-multi-release/approve-milestone}/useApproveMilestone.ts +16 -16
  39. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/button/ChangeMilestoneStatus.tsx +4 -4
  40. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/dialog/ChangeMilestoneStatus.tsx +4 -4
  41. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/form/ChangeMilestoneStatus.tsx +3 -3
  42. package/templates/escrows/{single-release/change-milestone-status/shared → single-multi-release/change-milestone-status}/useChangeMilestoneStatus.ts +1 -1
  43. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/button/FundEscrow.tsx +3 -3
  44. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/dialog/FundEscrow.tsx +3 -3
  45. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/form/FundEscrow.tsx +3 -3
  46. package/templates/escrows/{single-release/fund-escrow/shared → single-multi-release/fund-escrow}/useFundEscrow.ts +1 -1
  47. package/templates/escrows/single-release/dispute-escrow/button/DisputeEscrow.tsx +2 -2
  48. package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +14 -6
  49. package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +14 -6
  50. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -57
  51. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +42 -1
  52. package/templates/escrows/single-release/release-escrow/button/ReleaseEscrow.tsx +2 -2
  53. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +3 -3
  54. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +3 -6
  55. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +2 -2
  56. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +14 -1
  57. package/templates/escrows/single-release/update-escrow/dialog/UpdateEscrow.tsx +2 -2
  58. package/templates/escrows/single-release/update-escrow/form/UpdateEscrow.tsx +2 -2
  59. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +12 -7
  60. package/templates/providers/EscrowDialogsProvider.tsx +1 -3
  61. package/templates/providers/EscrowProvider.tsx +27 -4
  62. package/templates/providers/TrustlessWork.tsx +1 -1
  63. package/templates/escrows/details/ProgressEscrow.tsx +0 -191
  64. /package/templates/escrows/{single-release/approve-milestone/shared → single-multi-release/approve-milestone}/schema.ts +0 -0
  65. /package/templates/escrows/{single-release/change-milestone-status/shared → single-multi-release/change-milestone-status}/schema.ts +0 -0
  66. /package/templates/escrows/{single-release/fund-escrow/shared → single-multi-release/fund-escrow}/schema.ts +0 -0
@@ -0,0 +1,506 @@
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 { Input } from "__UI_BASE__/input";
11
+ import { Button } from "__UI_BASE__/button";
12
+ import { Card } from "__UI_BASE__/card";
13
+ import {
14
+ Select,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ SelectContent,
18
+ SelectItem,
19
+ } from "__UI_BASE__/select";
20
+ import { Textarea } from "__UI_BASE__/textarea";
21
+ import { useInitializeEscrow } from "./useInitializeEscrow";
22
+ import { Trash2, DollarSign, Percent, Loader2 } from "lucide-react";
23
+ import Link from "next/link";
24
+ import { trustlineOptions } from "@/components/tw-blocks/wallet-kit/trustlines";
25
+
26
+ export const InitializeEscrowForm = () => {
27
+ const {
28
+ form,
29
+ isSubmitting,
30
+ milestones,
31
+ isAnyMilestoneEmpty,
32
+ handleSubmit,
33
+ handleAddMilestone,
34
+ handleRemoveMilestone,
35
+ fillTemplateForm,
36
+ } = useInitializeEscrow();
37
+
38
+ const handleMilestoneAmountChange = (
39
+ index: number,
40
+ e: React.ChangeEvent<HTMLInputElement>
41
+ ) => {
42
+ let rawValue = e.target.value;
43
+ rawValue = rawValue.replace(/[^0-9.]/g, "");
44
+
45
+ if (rawValue.split(".").length > 2) {
46
+ rawValue = rawValue.slice(0, -1);
47
+ }
48
+
49
+ // Limit to 2 decimal places
50
+ if (rawValue.includes(".")) {
51
+ const parts = rawValue.split(".");
52
+ if (parts[1] && parts[1].length > 2) {
53
+ rawValue = parts[0] + "." + parts[1].slice(0, 2);
54
+ }
55
+ }
56
+
57
+ // Always keep as string to allow partial input like "0." or "0.5"
58
+ const updatedMilestones = [...milestones];
59
+ updatedMilestones[index] = {
60
+ ...updatedMilestones[index],
61
+ amount: rawValue,
62
+ };
63
+ form.setValue("milestones", updatedMilestones);
64
+ };
65
+
66
+ const handlePlatformFeeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
67
+ let rawValue = e.target.value;
68
+ rawValue = rawValue.replace(/[^0-9.]/g, "");
69
+
70
+ if (rawValue.split(".").length > 2) {
71
+ rawValue = rawValue.slice(0, -1);
72
+ }
73
+
74
+ // Limit to 2 decimal places
75
+ if (rawValue.includes(".")) {
76
+ const parts = rawValue.split(".");
77
+ if (parts[1] && parts[1].length > 2) {
78
+ rawValue = parts[0] + "." + parts[1].slice(0, 2);
79
+ }
80
+ }
81
+
82
+ // Always keep as string to allow partial input like "0." or "0.5"
83
+ form.setValue("platformFee", rawValue);
84
+ };
85
+
86
+ return (
87
+ <Form {...form}>
88
+ <form onSubmit={handleSubmit} className="flex flex-col space-y-6">
89
+ <Card className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 p-4">
90
+ <Link
91
+ className="flex-1"
92
+ href="https://docs.trustlesswork.com/trustless-work/technology-overview/escrow-types"
93
+ target="_blank"
94
+ >
95
+ <div className="flex items-center gap-2">
96
+ <div className="h-2 w-2 rounded-full bg-primary" />
97
+ <h2 className="text-xl font-semibold">Multi Release Escrow</h2>
98
+ </div>
99
+ <p className="text-muted-foreground mt-1">
100
+ Fill out the form to initialize a multi release escrow
101
+ </p>
102
+ </Link>
103
+ {process.env.NODE_ENV !== "production" && (
104
+ <Button
105
+ type="button"
106
+ variant="outline"
107
+ onClick={fillTemplateForm}
108
+ className="cursor-pointer"
109
+ >
110
+ Autofill
111
+ </Button>
112
+ )}
113
+ </Card>
114
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
115
+ <FormField
116
+ control={form.control}
117
+ name="title"
118
+ render={({ field }) => (
119
+ <FormItem>
120
+ <FormLabel className="flex items-center">
121
+ Title<span className="text-destructive ml-1">*</span>
122
+ </FormLabel>
123
+ <FormControl>
124
+ <Input
125
+ placeholder="Escrow title"
126
+ {...field}
127
+ onChange={(e) => {
128
+ field.onChange(e);
129
+ }}
130
+ />
131
+ </FormControl>
132
+ <FormMessage />
133
+ </FormItem>
134
+ )}
135
+ />
136
+
137
+ <FormField
138
+ control={form.control}
139
+ name="engagementId"
140
+ render={({ field }) => (
141
+ <FormItem>
142
+ <FormLabel className="flex items-center">
143
+ Engagement<span className="text-destructive ml-1">*</span>
144
+ </FormLabel>
145
+ <FormControl>
146
+ <Input
147
+ placeholder="Enter identifier"
148
+ {...field}
149
+ onChange={(e) => {
150
+ field.onChange(e);
151
+ }}
152
+ />
153
+ </FormControl>
154
+ <FormMessage />
155
+ </FormItem>
156
+ )}
157
+ />
158
+
159
+ <FormField
160
+ control={form.control}
161
+ name="trustline.address"
162
+ render={({ field }) => (
163
+ <FormItem>
164
+ <FormLabel className="flex items-center">
165
+ Trustline<span className="text-destructive ml-1">*</span>
166
+ </FormLabel>
167
+ <FormControl>
168
+ <Select
169
+ value={field.value}
170
+ onValueChange={(e) => {
171
+ field.onChange(e);
172
+ }}
173
+ >
174
+ <SelectTrigger className="w-full">
175
+ <SelectValue placeholder="Select trustline" />
176
+ </SelectTrigger>
177
+ <SelectContent>
178
+ {trustlineOptions
179
+ .filter((option) => option.value)
180
+ .map((option, index) => (
181
+ <SelectItem
182
+ key={`${option.value}-${index}`}
183
+ value={option.value}
184
+ >
185
+ {option.label}
186
+ </SelectItem>
187
+ ))}
188
+ </SelectContent>
189
+ </Select>
190
+ </FormControl>
191
+ <FormMessage />
192
+ </FormItem>
193
+ )}
194
+ />
195
+ </div>
196
+
197
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
198
+ <FormField
199
+ control={form.control}
200
+ name="roles.approver"
201
+ render={({ field }) => (
202
+ <FormItem>
203
+ <FormLabel className="flex items-center justify-between">
204
+ <span className="flex items-center">
205
+ Approver<span className="text-destructive ml-1">*</span>
206
+ </span>
207
+ </FormLabel>
208
+
209
+ <FormControl>
210
+ <Input
211
+ placeholder="Enter approver address"
212
+ {...field}
213
+ onChange={(e) => {
214
+ field.onChange(e);
215
+ }}
216
+ />
217
+ </FormControl>
218
+ <FormMessage />
219
+ </FormItem>
220
+ )}
221
+ />
222
+
223
+ <FormField
224
+ control={form.control}
225
+ name="roles.serviceProvider"
226
+ render={({ field }) => (
227
+ <FormItem>
228
+ <FormLabel className="flex items-center justify-between">
229
+ <span className="flex items-center">
230
+ Service Provider
231
+ <span className="text-destructive ml-1">*</span>
232
+ </span>
233
+ </FormLabel>
234
+
235
+ <FormControl>
236
+ <Input
237
+ placeholder="Enter service provider address"
238
+ {...field}
239
+ onChange={(e) => {
240
+ field.onChange(e);
241
+ }}
242
+ />
243
+ </FormControl>
244
+ <FormMessage />
245
+ </FormItem>
246
+ )}
247
+ />
248
+ </div>
249
+
250
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
251
+ <FormField
252
+ control={form.control}
253
+ name="roles.releaseSigner"
254
+ render={({ field }) => (
255
+ <FormItem>
256
+ <FormLabel className="flex items-center justify-between">
257
+ <span className="flex items-center">
258
+ Release Signer
259
+ <span className="text-destructive ml-1">*</span>
260
+ </span>
261
+ </FormLabel>
262
+
263
+ <FormControl>
264
+ <Input
265
+ placeholder="Enter release signer address"
266
+ {...field}
267
+ onChange={(e) => {
268
+ field.onChange(e);
269
+ }}
270
+ />
271
+ </FormControl>
272
+ <FormMessage />
273
+ </FormItem>
274
+ )}
275
+ />
276
+
277
+ <FormField
278
+ control={form.control}
279
+ name="roles.disputeResolver"
280
+ render={({ field }) => (
281
+ <FormItem>
282
+ <FormLabel className="flex items-center justify-between">
283
+ <span className="flex items-center">
284
+ Dispute Resolver
285
+ <span className="text-destructive ml-1">*</span>
286
+ </span>
287
+ </FormLabel>
288
+
289
+ <FormControl>
290
+ <Input
291
+ placeholder="Enter dispute resolver address"
292
+ {...field}
293
+ onChange={(e) => {
294
+ field.onChange(e);
295
+ }}
296
+ />
297
+ </FormControl>
298
+ <FormMessage />
299
+ </FormItem>
300
+ )}
301
+ />
302
+ </div>
303
+
304
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
305
+ <FormField
306
+ control={form.control}
307
+ name="roles.platformAddress"
308
+ render={({ field }) => (
309
+ <FormItem>
310
+ <FormLabel className="flex items-center justify-between">
311
+ <span className="flex items-center">
312
+ Platform Address
313
+ <span className="text-destructive ml-1">*</span>
314
+ </span>
315
+ </FormLabel>
316
+
317
+ <FormControl>
318
+ <Input
319
+ placeholder="Enter platform address"
320
+ {...field}
321
+ onChange={(e) => {
322
+ field.onChange(e);
323
+ }}
324
+ />
325
+ </FormControl>
326
+ <FormMessage />
327
+ </FormItem>
328
+ )}
329
+ />
330
+ <FormField
331
+ control={form.control}
332
+ name="roles.receiver"
333
+ render={({ field }) => (
334
+ <FormItem>
335
+ <FormLabel className="flex items-center justify-between">
336
+ <span className="flex items-center">
337
+ Receiver<span className="text-destructive ml-1">*</span>
338
+ </span>
339
+ </FormLabel>
340
+
341
+ <FormControl>
342
+ <Input
343
+ placeholder="Enter receiver address"
344
+ {...field}
345
+ onChange={(e) => {
346
+ field.onChange(e);
347
+ }}
348
+ />
349
+ </FormControl>
350
+ <FormMessage />
351
+ </FormItem>
352
+ )}
353
+ />
354
+ </div>
355
+
356
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
357
+ <FormField
358
+ control={form.control}
359
+ name="platformFee"
360
+ render={() => (
361
+ <FormItem>
362
+ <FormLabel className="flex items-center">
363
+ Platform Fee<span className="text-destructive ml-1">*</span>
364
+ </FormLabel>
365
+ <FormControl>
366
+ <div className="relative">
367
+ <Percent
368
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
369
+ size={18}
370
+ />
371
+ <Input
372
+ placeholder="Enter platform fee"
373
+ className="pl-10"
374
+ value={form.watch("platformFee")?.toString() || ""}
375
+ onChange={handlePlatformFeeChange}
376
+ />
377
+ </div>
378
+ </FormControl>
379
+ <FormMessage />
380
+ </FormItem>
381
+ )}
382
+ />
383
+
384
+ <FormField
385
+ control={form.control}
386
+ name="receiverMemo"
387
+ render={({ field }) => (
388
+ <FormItem>
389
+ <FormLabel className="flex items-center">
390
+ Receiver Memo (opcional)
391
+ </FormLabel>
392
+ <FormControl>
393
+ <Input
394
+ type="text"
395
+ placeholder="Enter the escrow receiver Memo"
396
+ {...field}
397
+ onChange={(e) => {
398
+ field.onChange(e);
399
+ }}
400
+ />
401
+ </FormControl>
402
+ <FormMessage />
403
+ </FormItem>
404
+ )}
405
+ />
406
+ </div>
407
+
408
+ <FormField
409
+ control={form.control}
410
+ name="description"
411
+ render={({ field }) => (
412
+ <FormItem>
413
+ <FormLabel className="flex items-center">
414
+ Description<span className="text-destructive ml-1">*</span>
415
+ </FormLabel>
416
+ <FormControl>
417
+ <Textarea
418
+ placeholder="Escrow description"
419
+ {...field}
420
+ onChange={(e) => {
421
+ field.onChange(e);
422
+ }}
423
+ />
424
+ </FormControl>
425
+ <FormMessage />
426
+ </FormItem>
427
+ )}
428
+ />
429
+
430
+ <div className="space-y-4">
431
+ <FormLabel className="flex items-center">
432
+ Milestones<span className="text-destructive ml-1">*</span>
433
+ </FormLabel>
434
+ {milestones.map((milestone, index) => (
435
+ <div key={index} className="space-y-4">
436
+ <div className="flex flex-col sm:flex-row items-start sm:items-center space-y-2 sm:space-y-0 sm:space-x-4">
437
+ <Input
438
+ placeholder="Milestone Description"
439
+ value={milestone.description}
440
+ className="w-full sm:w-3/5"
441
+ onChange={(e) => {
442
+ const updatedMilestones = [...milestones];
443
+ updatedMilestones[index].description = e.target.value;
444
+ form.setValue("milestones", updatedMilestones);
445
+ }}
446
+ />
447
+
448
+ <div className="relative w-full sm:w-2/5">
449
+ <DollarSign
450
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
451
+ size={18}
452
+ />
453
+ <Input
454
+ className="pl-10"
455
+ placeholder="Enter amount"
456
+ value={milestone.amount?.toString() || ""}
457
+ onChange={(e) => handleMilestoneAmountChange(index, e)}
458
+ />
459
+ </div>
460
+
461
+ <Button
462
+ onClick={() => handleRemoveMilestone(index)}
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
+ disabled={milestones.length === 1}
465
+ >
466
+ <Trash2 className="h-5 w-5" />
467
+ </Button>
468
+ </div>
469
+
470
+ {index === milestones.length - 1 && (
471
+ <div className="flex justify-end mt-4">
472
+ <Button
473
+ disabled={isAnyMilestoneEmpty}
474
+ className="w-full md:w-1/4"
475
+ variant="outline"
476
+ onClick={handleAddMilestone}
477
+ type="button"
478
+ >
479
+ Add Item
480
+ </Button>
481
+ </div>
482
+ )}
483
+ </div>
484
+ ))}
485
+ </div>
486
+
487
+ <div className="flex justify-start">
488
+ <Button
489
+ className="w-full md:w-1/4 cursor-pointer"
490
+ type="submit"
491
+ disabled={isAnyMilestoneEmpty || isSubmitting}
492
+ >
493
+ {isSubmitting ? (
494
+ <div className="flex items-center">
495
+ <Loader2 className="h-5 w-5 animate-spin" />
496
+ <span className="ml-2">Deploying...</span>
497
+ </div>
498
+ ) : (
499
+ "Deploy"
500
+ )}
501
+ </Button>
502
+ </div>
503
+ </form>
504
+ </Form>
505
+ );
506
+ };
@@ -0,0 +1,179 @@
1
+ import { z } from "zod";
2
+ import { isValidWallet } from "../../../../wallet-kit/validators";
3
+
4
+ export const useInitializeEscrowSchema = () => {
5
+ const getBaseSchema = () => {
6
+ return z.object({
7
+ trustline: z.object({
8
+ address: z.string().min(1, {
9
+ message: "Trustline address is required.",
10
+ }),
11
+ decimals: z.number().default(10000000),
12
+ }),
13
+ roles: z.object({
14
+ approver: z
15
+ .string()
16
+ .min(1, {
17
+ message: "Approver is required.",
18
+ })
19
+ .refine((value) => isValidWallet(value), {
20
+ message: "Approver must be a valid wallet.",
21
+ }),
22
+ serviceProvider: z
23
+ .string()
24
+ .min(1, {
25
+ message: "Service provider is required.",
26
+ })
27
+ .refine((value) => isValidWallet(value), {
28
+ message: "Service provider must be a valid wallet.",
29
+ }),
30
+ platformAddress: z
31
+ .string()
32
+ .min(1, {
33
+ message: "Platform address is required.",
34
+ })
35
+ .refine((value) => isValidWallet(value), {
36
+ message: "Platform address must be a valid wallet.",
37
+ }),
38
+ releaseSigner: z
39
+ .string()
40
+ .min(1, {
41
+ message: "Release signer is required.",
42
+ })
43
+ .refine((value) => isValidWallet(value), {
44
+ message: "Release signer must be a valid wallet.",
45
+ }),
46
+ disputeResolver: z
47
+ .string()
48
+ .min(1, {
49
+ message: "Dispute resolver is required.",
50
+ })
51
+ .refine((value) => isValidWallet(value), {
52
+ message: "Dispute resolver must be a valid wallet.",
53
+ }),
54
+ receiver: z
55
+ .string()
56
+ .min(1, {
57
+ message: "Receiver address is required.",
58
+ })
59
+ .refine((value) => isValidWallet(value), {
60
+ message: "Receiver address must be a valid wallet.",
61
+ }),
62
+ }),
63
+ engagementId: z.string().min(1, {
64
+ message: "Engagement is required.",
65
+ }),
66
+ title: z.string().min(1, {
67
+ message: "Title is required.",
68
+ }),
69
+ description: z.string().min(10, {
70
+ message: "Description must be at least 10 characters long.",
71
+ }),
72
+ platformFee: z
73
+ .union([z.string(), z.number()])
74
+ .refine(
75
+ (val) => {
76
+ if (typeof val === "string") {
77
+ if (val === "" || val === "." || val.endsWith(".")) {
78
+ return true;
79
+ }
80
+ const numVal = Number(val);
81
+ return !isNaN(numVal) && numVal > 0;
82
+ }
83
+ return val > 0;
84
+ },
85
+ {
86
+ message: "Platform fee must be greater than 0.",
87
+ }
88
+ )
89
+ .refine(
90
+ (val) => {
91
+ if (typeof val === "string") {
92
+ if (val === "" || val === "." || val.endsWith(".")) {
93
+ return true;
94
+ }
95
+ const numVal = Number(val);
96
+ if (isNaN(numVal)) return false;
97
+ const decimalPlaces = (numVal.toString().split(".")[1] || "")
98
+ .length;
99
+ return decimalPlaces <= 2;
100
+ }
101
+ const decimalPlaces = (val.toString().split(".")[1] || "").length;
102
+ return decimalPlaces <= 2;
103
+ },
104
+ {
105
+ message: "Platform fee can have a maximum of 2 decimal places.",
106
+ }
107
+ ),
108
+ receiverMemo: z
109
+ .string()
110
+ .optional()
111
+ .refine((val) => !val || val.length >= 1, {
112
+ message: "Receiver Memo must be at least 1.",
113
+ })
114
+ .refine((val) => !val || /^[1-9][0-9]*$/.test(val), {
115
+ message:
116
+ "Receiver Memo must be a whole number greater than 0 (no decimals).",
117
+ }),
118
+ });
119
+ };
120
+
121
+ const getMultiReleaseFormSchema = () => {
122
+ const baseSchema = getBaseSchema();
123
+
124
+ return baseSchema.extend({
125
+ milestones: z
126
+ .array(
127
+ z.object({
128
+ description: z.string().min(1, {
129
+ message: "Milestone description is required.",
130
+ }),
131
+ amount: z
132
+ .union([z.string(), z.number()])
133
+ .refine(
134
+ (val) => {
135
+ if (typeof val === "string") {
136
+ if (val === "" || val === "." || val.endsWith(".")) {
137
+ return true;
138
+ }
139
+ const numVal = Number(val);
140
+ return !isNaN(numVal) && numVal > 0;
141
+ }
142
+ return val > 0;
143
+ },
144
+ {
145
+ message: "Milestone amount must be greater than 0.",
146
+ }
147
+ )
148
+ .refine(
149
+ (val) => {
150
+ if (typeof val === "string") {
151
+ if (val === "" || val === "." || val.endsWith(".")) {
152
+ return true;
153
+ }
154
+ const numVal = Number(val);
155
+ if (isNaN(numVal)) return false;
156
+ const decimalPlaces = (
157
+ numVal.toString().split(".")[1] || ""
158
+ ).length;
159
+ return decimalPlaces <= 2;
160
+ }
161
+ const decimalPlaces = (val.toString().split(".")[1] || "")
162
+ .length;
163
+ return decimalPlaces <= 2;
164
+ },
165
+ {
166
+ message:
167
+ "Milestone amount can have a maximum of 2 decimal places.",
168
+ }
169
+ ),
170
+ })
171
+ )
172
+ .min(1, { message: "At least one milestone is required." }),
173
+ });
174
+ };
175
+
176
+ return {
177
+ getMultiReleaseFormSchema,
178
+ };
179
+ };