@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,175 @@
1
+ import * as React from "react";
2
+ import { useForm } from "react-hook-form";
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { useInitializeEscrowSchema } from "./schema";
5
+ import { z } from "zod";
6
+ import {
7
+ InitializeMultiReleaseEscrowPayload,
8
+ InitializeMultiReleaseEscrowResponse,
9
+ } from "@trustless-work/escrow/types";
10
+ import { toast } from "sonner";
11
+ import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
12
+ import { useEscrowsMutations } from "@/components/tw-blocks/tanstack/useEscrowsMutations";
13
+ import {
14
+ ErrorResponse,
15
+ handleError,
16
+ } from "@/components/tw-blocks/handle-errors/handle";
17
+ import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
18
+ import { trustlineOptions } from "@/components/tw-blocks/wallet-kit/trustlines";
19
+
20
+ export function useInitializeEscrow() {
21
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
22
+
23
+ const { getMultiReleaseFormSchema } = useInitializeEscrowSchema();
24
+ const formSchema = getMultiReleaseFormSchema();
25
+ const { setSelectedEscrow } = useEscrowContext();
26
+
27
+ const { walletAddress } = useWalletContext();
28
+ const { deployEscrow } = useEscrowsMutations();
29
+
30
+ const form = useForm<z.infer<typeof formSchema>>({
31
+ resolver: zodResolver(formSchema),
32
+ defaultValues: {
33
+ engagementId: "",
34
+ title: "",
35
+ description: "",
36
+ platformFee: undefined,
37
+ receiverMemo: "",
38
+ trustline: {
39
+ address: "",
40
+ decimals: 10000000,
41
+ },
42
+ roles: {
43
+ approver: "",
44
+ serviceProvider: "",
45
+ platformAddress: "",
46
+ receiver: "",
47
+ releaseSigner: "",
48
+ disputeResolver: "",
49
+ },
50
+ milestones: [{ description: "", amount: "" }],
51
+ },
52
+ mode: "onChange",
53
+ });
54
+
55
+ const milestones = form.watch("milestones");
56
+ const isAnyMilestoneEmpty = milestones.some(
57
+ (milestone) => milestone.description === ""
58
+ );
59
+
60
+ const handleAddMilestone = () => {
61
+ const currentMilestones = form.getValues("milestones");
62
+ const updatedMilestones = [
63
+ ...currentMilestones,
64
+ { description: "", amount: "" },
65
+ ];
66
+ form.setValue("milestones", updatedMilestones);
67
+ };
68
+
69
+ const handleRemoveMilestone = (index: number) => {
70
+ const currentMilestones = form.getValues("milestones");
71
+ const updatedMilestones = currentMilestones.filter((_, i) => i !== index);
72
+ form.setValue("milestones", updatedMilestones);
73
+ };
74
+
75
+ const fillTemplateForm = () => {
76
+ const usdc = trustlineOptions.find((t) => t.label === "USDC");
77
+
78
+ const templateData: z.infer<typeof formSchema> = {
79
+ engagementId: "ENG-001",
80
+ title: "Design Landing Page",
81
+ description: "Landing for the new product of the company.",
82
+ platformFee: 5,
83
+ receiverMemo: "123",
84
+ trustline: {
85
+ address: usdc?.value || "",
86
+ decimals: 10000000,
87
+ },
88
+ roles: {
89
+ approver: walletAddress || "",
90
+ serviceProvider: walletAddress || "",
91
+ platformAddress: walletAddress || "",
92
+ receiver: walletAddress || "",
93
+ releaseSigner: walletAddress || "",
94
+ disputeResolver: walletAddress || "",
95
+ },
96
+ milestones: [
97
+ { description: "Design the wireframe", amount: 2 },
98
+ { description: "Develop the wireframe", amount: 2 },
99
+ { description: "Deploy the wireframe", amount: 2 },
100
+ ],
101
+ };
102
+
103
+ // Set form values
104
+ Object.entries(templateData).forEach(([key, value]) => {
105
+ form.setValue(key as keyof z.infer<typeof formSchema>, value);
106
+ });
107
+
108
+ // Explicitly set the trustline field
109
+ form.setValue("trustline.address", usdc?.value || "");
110
+ form.setValue("trustline.decimals", 10000000);
111
+ };
112
+
113
+ const handleSubmit = form.handleSubmit(async (payload) => {
114
+ try {
115
+ setIsSubmitting(true);
116
+
117
+ /**
118
+ * Create the final payload for the initialize escrow mutation
119
+ *
120
+ * @param payload - The payload from the form
121
+ * @returns The final payload for the initialize escrow mutation
122
+ */
123
+ const finalPayload: InitializeMultiReleaseEscrowPayload = {
124
+ ...payload,
125
+ platformFee:
126
+ typeof payload.platformFee === "string"
127
+ ? Number(payload.platformFee)
128
+ : payload.platformFee,
129
+ receiverMemo: Number(payload.receiverMemo) ?? 0,
130
+ signer: walletAddress || "",
131
+ milestones: payload.milestones.map((milestone) => ({
132
+ ...milestone,
133
+ amount:
134
+ typeof milestone.amount === "string"
135
+ ? Number(milestone.amount)
136
+ : milestone.amount,
137
+ })),
138
+ };
139
+
140
+ /**
141
+ * Call the initialize escrow mutation
142
+ *
143
+ * @param payload - The final payload for the initialize escrow mutation
144
+ * @param type - The type of the escrow
145
+ * @param address - The address of the escrow
146
+ */
147
+ const response: InitializeMultiReleaseEscrowResponse =
148
+ (await deployEscrow.mutateAsync({
149
+ payload: finalPayload,
150
+ type: "multi-release",
151
+ address: walletAddress || "",
152
+ })) as InitializeMultiReleaseEscrowResponse;
153
+
154
+ toast.success("Escrow initialized successfully");
155
+
156
+ setSelectedEscrow({ ...finalPayload, contractId: response.contractId });
157
+ } catch (error) {
158
+ toast.error(handleError(error as ErrorResponse).message);
159
+ } finally {
160
+ setIsSubmitting(false);
161
+ form.reset();
162
+ }
163
+ });
164
+
165
+ return {
166
+ form,
167
+ isSubmitting,
168
+ milestones,
169
+ isAnyMilestoneEmpty,
170
+ fillTemplateForm,
171
+ handleSubmit,
172
+ handleAddMilestone,
173
+ handleRemoveMilestone,
174
+ };
175
+ }
@@ -0,0 +1,116 @@
1
+ import * as React from "react";
2
+ import { Button } from "__UI_BASE__/button";
3
+ import { useEscrowsMutations } from "@/components/tw-blocks/tanstack/useEscrowsMutations";
4
+ import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
5
+ import {
6
+ MultiReleaseReleaseFundsPayload,
7
+ MultiReleaseMilestone,
8
+ } from "@trustless-work/escrow/types";
9
+ import { toast } from "sonner";
10
+ import {
11
+ ErrorResponse,
12
+ handleError,
13
+ } from "@/components/tw-blocks/handle-errors/handle";
14
+ import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
15
+ import { useEscrowDialogs } from "@/components/tw-blocks/providers/EscrowDialogsProvider";
16
+ import { useEscrowAmountContext } from "@/components/tw-blocks/providers/EscrowAmountProvider";
17
+ import { Loader2 } from "lucide-react";
18
+
19
+ type ReleaseEscrowButtonProps = {
20
+ milestoneIndex: number | string;
21
+ };
22
+
23
+ export const ReleaseEscrowButton = ({
24
+ milestoneIndex,
25
+ }: ReleaseEscrowButtonProps) => {
26
+ const { releaseFunds } = useEscrowsMutations();
27
+ const { selectedEscrow, updateEscrow } = useEscrowContext();
28
+ const dialogStates = useEscrowDialogs();
29
+ const { setAmounts } = useEscrowAmountContext();
30
+ const { walletAddress } = useWalletContext();
31
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
32
+
33
+ async function handleClick() {
34
+ try {
35
+ setIsSubmitting(true);
36
+
37
+ /**
38
+ * Create the payload for the release escrow mutation
39
+ *
40
+ * @returns The payload for the release escrow mutation
41
+ */
42
+ const payload: MultiReleaseReleaseFundsPayload = {
43
+ contractId: selectedEscrow?.contractId || "",
44
+ releaseSigner: walletAddress || "",
45
+ milestoneIndex: String(milestoneIndex),
46
+ };
47
+
48
+ /**
49
+ * Call the release escrow mutation
50
+ *
51
+ * @param payload - The payload for the release escrow mutation
52
+ * @param type - The type of the escrow
53
+ * @param address - The address of the escrow
54
+ */
55
+ await releaseFunds.mutateAsync({
56
+ payload,
57
+ type: "multi-release",
58
+ address: walletAddress || "",
59
+ });
60
+
61
+ toast.success("Escrow released successfully");
62
+
63
+ // Ensure amounts are up to date for the success dialog
64
+ if (selectedEscrow) {
65
+ const milestone = selectedEscrow.milestones?.[Number(milestoneIndex)];
66
+ const releasedAmount = Number(
67
+ (milestone as MultiReleaseMilestone | undefined)?.amount || 0
68
+ );
69
+ const platformFee = Number(selectedEscrow.platformFee || 0);
70
+ setAmounts(releasedAmount, platformFee);
71
+ }
72
+
73
+ updateEscrow({
74
+ ...selectedEscrow,
75
+ milestones: selectedEscrow?.milestones.map((milestone, index) => {
76
+ if (index === Number(milestoneIndex)) {
77
+ return {
78
+ ...milestone,
79
+ flags: {
80
+ ...(milestone as MultiReleaseMilestone).flags,
81
+ released: true,
82
+ },
83
+ };
84
+ }
85
+ return milestone;
86
+ }),
87
+ balance: (selectedEscrow?.balance || 0) - (selectedEscrow?.amount || 0),
88
+ });
89
+
90
+ // Open success dialog
91
+ dialogStates.successRelease.setIsOpen(true);
92
+ } catch (error) {
93
+ toast.error(handleError(error as ErrorResponse).message);
94
+ } finally {
95
+ setIsSubmitting(false);
96
+ }
97
+ }
98
+
99
+ return (
100
+ <Button
101
+ type="button"
102
+ disabled={isSubmitting}
103
+ onClick={handleClick}
104
+ className="cursor-pointer w-full"
105
+ >
106
+ {isSubmitting ? (
107
+ <div className="flex items-center">
108
+ <Loader2 className="h-5 w-5 animate-spin" />
109
+ <span className="ml-2">Releasing...</span>
110
+ </div>
111
+ ) : (
112
+ "Release Milestone"
113
+ )}
114
+ </Button>
115
+ );
116
+ };
@@ -0,0 +1,122 @@
1
+ import * as React from "react";
2
+ import { Button } from "__UI_BASE__/button";
3
+ import { useEscrowsMutations } from "@/components/tw-blocks/tanstack/useEscrowsMutations";
4
+ import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
5
+ import {
6
+ MultiReleaseResolveDisputePayload,
7
+ MultiReleaseMilestone,
8
+ } from "@trustless-work/escrow/types";
9
+ import { toast } from "sonner";
10
+ import {
11
+ ErrorResponse,
12
+ handleError,
13
+ } from "@/components/tw-blocks/handle-errors/handle";
14
+ import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
15
+ import { Loader2 } from "lucide-react";
16
+
17
+ type ResolveDisputeButtonProps = {
18
+ approverFunds: number;
19
+ receiverFunds: number;
20
+ milestoneIndex: number | string;
21
+ };
22
+
23
+ export const ResolveDisputeButton = ({
24
+ approverFunds,
25
+ receiverFunds,
26
+ milestoneIndex,
27
+ }: ResolveDisputeButtonProps) => {
28
+ const { resolveDispute } = useEscrowsMutations();
29
+ const { selectedEscrow, updateEscrow } = useEscrowContext();
30
+ const { walletAddress } = useWalletContext();
31
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
32
+
33
+ async function handleClick() {
34
+ 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");
47
+ return;
48
+ }
49
+
50
+ setIsSubmitting(true);
51
+
52
+ /**
53
+ * Create the payload for the resolve dispute mutation
54
+ *
55
+ * @returns The payload for the resolve dispute mutation
56
+ */
57
+ const payload: MultiReleaseResolveDisputePayload = {
58
+ contractId: selectedEscrow?.contractId || "",
59
+ disputeResolver: walletAddress || "",
60
+ approverFunds: Number(approverFunds),
61
+ receiverFunds: Number(receiverFunds),
62
+ milestoneIndex: String(milestoneIndex),
63
+ };
64
+
65
+ /**
66
+ * Call the resolve dispute mutation
67
+ *
68
+ * @param payload - The payload for the resolve dispute mutation
69
+ * @param type - The type of the escrow
70
+ * @param address - The address of the escrow
71
+ */
72
+ await resolveDispute.mutateAsync({
73
+ payload,
74
+ type: "multi-release",
75
+ address: walletAddress || "",
76
+ });
77
+
78
+ toast.success("Dispute resolved successfully");
79
+ updateEscrow({
80
+ ...selectedEscrow,
81
+ milestones: selectedEscrow?.milestones.map((milestone, index) => {
82
+ if (index === Number(milestoneIndex)) {
83
+ return {
84
+ ...milestone,
85
+ flags: {
86
+ ...(milestone as MultiReleaseMilestone).flags,
87
+ disputed: false,
88
+ resolved: true,
89
+ },
90
+ };
91
+ }
92
+ return milestone;
93
+ }),
94
+ balance:
95
+ selectedEscrow?.balance ||
96
+ Number(approverFunds) + Number(receiverFunds),
97
+ });
98
+ } catch (error) {
99
+ toast.error(handleError(error as ErrorResponse).message);
100
+ } finally {
101
+ setIsSubmitting(false);
102
+ }
103
+ }
104
+
105
+ return (
106
+ <Button
107
+ type="button"
108
+ disabled={isSubmitting}
109
+ onClick={handleClick}
110
+ className="cursor-pointer w-full"
111
+ >
112
+ {isSubmitting ? (
113
+ <div className="flex items-center">
114
+ <Loader2 className="h-5 w-5 animate-spin" />
115
+ <span className="ml-2">Resolving...</span>
116
+ </div>
117
+ ) : (
118
+ "Resolve Dispute"
119
+ )}
120
+ </Button>
121
+ );
122
+ };
@@ -0,0 +1,178 @@
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 {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ DialogTrigger,
18
+ } from "__UI_BASE__/dialog";
19
+ import { Loader2 } from "lucide-react";
20
+ import { useResolveDispute } from "./useResolveDispute";
21
+ import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
22
+ import { formatCurrency } from "../../../../helpers/format.helper";
23
+ import {
24
+ Select,
25
+ SelectContent,
26
+ SelectItem,
27
+ SelectTrigger,
28
+ SelectValue,
29
+ } from "__UI_BASE__/select";
30
+
31
+ export const ResolveDisputeDialog = ({
32
+ showSelectMilestone = false,
33
+ milestoneIndex,
34
+ }: {
35
+ showSelectMilestone?: boolean;
36
+ milestoneIndex?: number | string;
37
+ }) => {
38
+ const { form, handleSubmit, isSubmitting, totalAmount } = useResolveDispute();
39
+ const { selectedEscrow } = useEscrowContext();
40
+
41
+ React.useEffect(() => {
42
+ if (
43
+ !showSelectMilestone &&
44
+ milestoneIndex !== undefined &&
45
+ milestoneIndex !== null
46
+ ) {
47
+ form.setValue("milestoneIndex", String(milestoneIndex));
48
+ }
49
+ }, [showSelectMilestone, milestoneIndex, form]);
50
+
51
+ return (
52
+ <Dialog>
53
+ <DialogTrigger asChild>
54
+ <Button type="button" className="cursor-pointer w-full">
55
+ Resolve Dispute
56
+ </Button>
57
+ </DialogTrigger>
58
+ <DialogContent>
59
+ <DialogHeader>
60
+ <DialogTitle>Resolve Dispute</DialogTitle>
61
+ </DialogHeader>
62
+ <Form {...form}>
63
+ <form onSubmit={handleSubmit}>
64
+ {showSelectMilestone && (
65
+ <FormField
66
+ control={form.control}
67
+ name="milestoneIndex"
68
+ render={({ field }) => (
69
+ <FormItem>
70
+ <FormLabel className="flex items-center">
71
+ Milestone
72
+ <span className="text-destructive ml-1">*</span>
73
+ </FormLabel>
74
+ <FormControl>
75
+ <Select
76
+ value={field.value}
77
+ onValueChange={(e) => {
78
+ field.onChange(e);
79
+ }}
80
+ >
81
+ <SelectTrigger className="w-full">
82
+ <SelectValue placeholder="Select milestone" />
83
+ </SelectTrigger>
84
+ <SelectContent>
85
+ {(selectedEscrow?.milestones || []).map((m, idx) => (
86
+ <SelectItem key={`ms-${idx}`} value={String(idx)}>
87
+ {m?.description || `Milestone ${idx + 1}`}
88
+ </SelectItem>
89
+ ))}
90
+ </SelectContent>
91
+ </Select>
92
+ </FormControl>
93
+ <FormMessage />
94
+ </FormItem>
95
+ )}
96
+ />
97
+ )}
98
+
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
+ />
119
+
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
+ />
139
+ </div>
140
+
141
+ <div className="mt-4 flex justify-between items-center">
142
+ <Button
143
+ type="submit"
144
+ disabled={isSubmitting}
145
+ className="cursor-pointer"
146
+ >
147
+ {isSubmitting ? (
148
+ <div className="flex items-center">
149
+ <Loader2 className="h-5 w-5 animate-spin" />
150
+ <span className="ml-2">Resolving...</span>
151
+ </div>
152
+ ) : (
153
+ "Resolve"
154
+ )}
155
+ </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
+ </div>
173
+ </form>
174
+ </Form>
175
+ </DialogContent>
176
+ </Dialog>
177
+ );
178
+ };