@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,232 @@
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 getSingleReleaseFormSchema = () => {
122
+ const baseSchema = getBaseSchema();
123
+
124
+ return baseSchema.extend({
125
+ amount: z
126
+ .union([z.string(), z.number()])
127
+ .refine(
128
+ (val) => {
129
+ if (typeof val === "string") {
130
+ if (val === "" || val === "." || val.endsWith(".")) {
131
+ return true;
132
+ }
133
+ const numVal = Number(val);
134
+ return !isNaN(numVal) && numVal > 0;
135
+ }
136
+ return val > 0;
137
+ },
138
+ {
139
+ message: "Amount must be greater than 0.",
140
+ }
141
+ )
142
+ .refine(
143
+ (val) => {
144
+ if (typeof val === "string") {
145
+ if (val === "" || val === "." || val.endsWith(".")) {
146
+ return true;
147
+ }
148
+ const numVal = Number(val);
149
+ if (isNaN(numVal)) return false;
150
+ const decimalPlaces = (numVal.toString().split(".")[1] || "")
151
+ .length;
152
+ return decimalPlaces <= 2;
153
+ }
154
+ const decimalPlaces = (val.toString().split(".")[1] || "").length;
155
+ return decimalPlaces <= 2;
156
+ },
157
+ {
158
+ message: "Amount can have a maximum of 2 decimal places.",
159
+ }
160
+ ),
161
+ milestones: z
162
+ .array(
163
+ z.object({
164
+ description: z.string().min(1, {
165
+ message: "Milestone description is required.",
166
+ }),
167
+ })
168
+ )
169
+ .min(1, { message: "At least one milestone is required." }),
170
+ });
171
+ };
172
+
173
+ const getMultiReleaseFormSchema = () => {
174
+ const baseSchema = getBaseSchema();
175
+
176
+ return baseSchema.extend({
177
+ milestones: z
178
+ .array(
179
+ z.object({
180
+ description: z.string().min(1, {
181
+ message: "Milestone description is required.",
182
+ }),
183
+ amount: z
184
+ .union([z.string(), z.number()])
185
+ .refine(
186
+ (val) => {
187
+ if (typeof val === "string") {
188
+ if (val === "" || val === "." || val.endsWith(".")) {
189
+ return true;
190
+ }
191
+ const numVal = Number(val);
192
+ return !isNaN(numVal) && numVal > 0;
193
+ }
194
+ return val > 0;
195
+ },
196
+ {
197
+ message: "Milestone amount must be greater than 0.",
198
+ }
199
+ )
200
+ .refine(
201
+ (val) => {
202
+ if (typeof val === "string") {
203
+ if (val === "" || val === "." || val.endsWith(".")) {
204
+ return true;
205
+ }
206
+ const numVal = Number(val);
207
+ if (isNaN(numVal)) return false;
208
+ const decimalPlaces = (
209
+ numVal.toString().split(".")[1] || ""
210
+ ).length;
211
+ return decimalPlaces <= 2;
212
+ }
213
+ const decimalPlaces = (val.toString().split(".")[1] || "")
214
+ .length;
215
+ return decimalPlaces <= 2;
216
+ },
217
+ {
218
+ message:
219
+ "Milestone amount can have a maximum of 2 decimal places.",
220
+ }
221
+ ),
222
+ })
223
+ )
224
+ .min(1, { message: "At least one milestone is required." }),
225
+ });
226
+ };
227
+
228
+ return {
229
+ getSingleReleaseFormSchema,
230
+ getMultiReleaseFormSchema,
231
+ };
232
+ };
@@ -0,0 +1,115 @@
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
+ InitializeSingleReleaseEscrowPayload,
8
+ InitializeSingleReleaseEscrowResponse,
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/tanstak/useEscrowsMutations";
13
+ import {
14
+ ErrorResponse,
15
+ handleError,
16
+ } from "@/components/tw-blocks/handle-errors/handle";
17
+
18
+ export function useInitializeEscrow() {
19
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
20
+
21
+ const { getSingleReleaseFormSchema } = useInitializeEscrowSchema();
22
+ const formSchema = getSingleReleaseFormSchema();
23
+
24
+ const { walletAddress } = useWalletContext();
25
+ const { deployEscrow } = useEscrowsMutations();
26
+
27
+ const form = useForm<z.infer<typeof formSchema>>({
28
+ resolver: zodResolver(formSchema),
29
+ defaultValues: {
30
+ engagementId: "",
31
+ title: "",
32
+ description: "",
33
+ platformFee: undefined,
34
+ amount: undefined,
35
+ receiverMemo: "",
36
+ trustline: {
37
+ address: "",
38
+ decimals: 10000000,
39
+ },
40
+ roles: {
41
+ approver: "",
42
+ serviceProvider: "",
43
+ platformAddress: "",
44
+ receiver: "",
45
+ releaseSigner: "",
46
+ disputeResolver: "",
47
+ },
48
+ milestones: [{ description: "" }],
49
+ },
50
+ mode: "onChange",
51
+ });
52
+
53
+ const milestones = form.watch("milestones");
54
+ const isAnyMilestoneEmpty = milestones.some(
55
+ (milestone) => milestone.description === ""
56
+ );
57
+
58
+ const handleAddMilestone = () => {
59
+ const currentMilestones = form.getValues("milestones");
60
+ const updatedMilestones = [...currentMilestones, { description: "" }];
61
+ form.setValue("milestones", updatedMilestones);
62
+ };
63
+
64
+ const handleRemoveMilestone = (index: number) => {
65
+ const currentMilestones = form.getValues("milestones");
66
+ const updatedMilestones = currentMilestones.filter((_, i) => i !== index);
67
+ form.setValue("milestones", updatedMilestones);
68
+ };
69
+
70
+ const handleSubmit = form.handleSubmit(async (payload) => {
71
+ try {
72
+ setIsSubmitting(true);
73
+
74
+ const finalPayload: InitializeSingleReleaseEscrowPayload = {
75
+ ...payload,
76
+ amount:
77
+ typeof payload.amount === "string"
78
+ ? Number(payload.amount)
79
+ : payload.amount,
80
+ platformFee:
81
+ typeof payload.platformFee === "string"
82
+ ? Number(payload.platformFee)
83
+ : payload.platformFee,
84
+ receiverMemo: Number(payload.receiverMemo) ?? 0,
85
+ signer: walletAddress || "",
86
+ milestones: payload.milestones,
87
+ };
88
+
89
+ const response: InitializeSingleReleaseEscrowResponse =
90
+ (await deployEscrow.mutateAsync({
91
+ payload: finalPayload,
92
+ type: "single-release",
93
+ address: walletAddress || "",
94
+ })) as InitializeSingleReleaseEscrowResponse;
95
+
96
+ console.log("response", response);
97
+ toast.success("Escrow initialized successfully");
98
+ } catch (error) {
99
+ toast.error(handleError(error as ErrorResponse).message);
100
+ } finally {
101
+ setIsSubmitting(false);
102
+ form.reset();
103
+ }
104
+ });
105
+
106
+ return {
107
+ form,
108
+ isSubmitting,
109
+ milestones,
110
+ isAnyMilestoneEmpty,
111
+ handleSubmit,
112
+ handleAddMilestone,
113
+ handleRemoveMilestone,
114
+ };
115
+ }
@@ -0,0 +1,80 @@
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 { SingleReleaseReleaseFundsPayload } 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 { useEscrowDialogs } from "../../../escrow-context/EscrowDialogsProvider";
13
+ import { useEscrowAmountContext } from "../../../escrow-context/EscrowAmountProvider";
14
+ import { Loader2 } from "lucide-react";
15
+
16
+ export default function ReleaseEscrowButton() {
17
+ const { releaseFunds } = useEscrowsMutations();
18
+ const { selectedEscrow, updateEscrow } = useEscrowContext();
19
+ const dialogStates = useEscrowDialogs();
20
+ const { setAmounts } = useEscrowAmountContext();
21
+ const { walletAddress } = useWalletContext();
22
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
23
+
24
+ async function handleClick() {
25
+ try {
26
+ setIsSubmitting(true);
27
+
28
+ const payload: SingleReleaseReleaseFundsPayload = {
29
+ contractId: selectedEscrow?.contractId || "",
30
+ releaseSigner: walletAddress || "",
31
+ };
32
+
33
+ await releaseFunds.mutateAsync({
34
+ payload,
35
+ type: "single-release",
36
+ address: walletAddress || "",
37
+ });
38
+
39
+ toast.success("Escrow released successfully");
40
+
41
+ // Ensure amounts are up to date for the success dialog
42
+ if (selectedEscrow) {
43
+ const totalAmount = Number(selectedEscrow.amount || 0);
44
+ const platformFee = Number(selectedEscrow.platformFee || 0);
45
+ setAmounts(totalAmount, platformFee);
46
+ }
47
+
48
+ updateEscrow({
49
+ ...selectedEscrow,
50
+ flags: { ...selectedEscrow?.flags, released: true },
51
+ balance: (selectedEscrow?.balance || 0) - (selectedEscrow?.amount || 0),
52
+ });
53
+
54
+ // Open success dialog
55
+ dialogStates.successRelease.setIsOpen(true);
56
+ } catch (error) {
57
+ toast.error(handleError(error as ErrorResponse).message);
58
+ } finally {
59
+ setIsSubmitting(false);
60
+ }
61
+ }
62
+
63
+ return (
64
+ <Button
65
+ type="button"
66
+ disabled={isSubmitting}
67
+ onClick={handleClick}
68
+ className="cursor-pointer w-full"
69
+ >
70
+ {isSubmitting ? (
71
+ <div className="flex items-center">
72
+ <Loader2 className="h-5 w-5 animate-spin" />
73
+ <span className="ml-2">Releasing...</span>
74
+ </div>
75
+ ) : (
76
+ "Release Escrow"
77
+ )}
78
+ </Button>
79
+ );
80
+ }
@@ -0,0 +1,94 @@
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 { SingleReleaseResolveDisputePayload } 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 ResolveDisputeButtonProps = {
15
+ approverFunds: number;
16
+ receiverFunds: number;
17
+ };
18
+
19
+ export default function ResolveDisputeButton({
20
+ approverFunds,
21
+ receiverFunds,
22
+ }: ResolveDisputeButtonProps) {
23
+ const { resolveDispute } = useEscrowsMutations();
24
+ const { selectedEscrow, updateEscrow } = useEscrowContext();
25
+ const { walletAddress } = useWalletContext();
26
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
27
+
28
+ async function handleClick() {
29
+ try {
30
+ if (
31
+ approverFunds == null ||
32
+ Number.isNaN(approverFunds) ||
33
+ receiverFunds == null ||
34
+ Number.isNaN(receiverFunds)
35
+ ) {
36
+ toast.error("Both amounts are required");
37
+ return;
38
+ }
39
+
40
+ if (approverFunds < 0 || receiverFunds < 0) {
41
+ toast.error("Amounts must be >= 0");
42
+ return;
43
+ }
44
+
45
+ setIsSubmitting(true);
46
+
47
+ const payload: SingleReleaseResolveDisputePayload = {
48
+ contractId: selectedEscrow?.contractId || "",
49
+ disputeResolver: walletAddress || "",
50
+ approverFunds: Number(approverFunds),
51
+ receiverFunds: Number(receiverFunds),
52
+ };
53
+
54
+ await resolveDispute.mutateAsync({
55
+ payload,
56
+ type: "single-release",
57
+ address: walletAddress || "",
58
+ });
59
+
60
+ toast.success("Dispute resolved successfully");
61
+ updateEscrow({
62
+ ...selectedEscrow,
63
+ flags: {
64
+ ...selectedEscrow?.flags,
65
+ disputed: false,
66
+ resolved: true,
67
+ },
68
+ balance: selectedEscrow?.balance || 0,
69
+ });
70
+ } catch (error) {
71
+ toast.error(handleError(error as ErrorResponse).message);
72
+ } finally {
73
+ setIsSubmitting(false);
74
+ }
75
+ }
76
+
77
+ return (
78
+ <Button
79
+ type="button"
80
+ disabled={isSubmitting}
81
+ onClick={handleClick}
82
+ className="cursor-pointer w-full"
83
+ >
84
+ {isSubmitting ? (
85
+ <div className="flex items-center">
86
+ <Loader2 className="h-5 w-5 animate-spin" />
87
+ <span className="ml-2">Resolving...</span>
88
+ </div>
89
+ ) : (
90
+ "Resolve Dispute"
91
+ )}
92
+ </Button>
93
+ );
94
+ }
@@ -0,0 +1,123 @@
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 "../../../escrow-context/EscrowProvider";
22
+
23
+ export default function ResolveDisputeDialog() {
24
+ const { form, handleSubmit, isSubmitting } = useResolveDispute();
25
+ const { selectedEscrow } = useEscrowContext();
26
+
27
+ function formatCurrency(amount: number, currency: string) {
28
+ return `${currency} ${amount.toFixed(2)}`;
29
+ }
30
+
31
+ return (
32
+ <Dialog>
33
+ <DialogTrigger asChild>
34
+ <Button type="button" className="cursor-pointer w-full">
35
+ Resolve Dispute
36
+ </Button>
37
+ </DialogTrigger>
38
+ <DialogContent>
39
+ <DialogHeader>
40
+ <DialogTitle>Resolve Dispute</DialogTitle>
41
+ </DialogHeader>
42
+ <Form {...form}>
43
+ <form onSubmit={handleSubmit}>
44
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
45
+ <FormField
46
+ control={form.control}
47
+ name="approverFunds"
48
+ render={({ field }) => (
49
+ <FormItem>
50
+ <FormLabel>Approver Funds</FormLabel>
51
+ <FormControl>
52
+ <Input
53
+ type="text"
54
+ inputMode="decimal"
55
+ placeholder="Enter approver funds"
56
+ value={field.value as unknown as string}
57
+ onChange={(e) => field.onChange(e.target.value)}
58
+ />
59
+ </FormControl>
60
+ <FormMessage />
61
+ </FormItem>
62
+ )}
63
+ />
64
+
65
+ <FormField
66
+ control={form.control}
67
+ name="receiverFunds"
68
+ render={({ field }) => (
69
+ <FormItem>
70
+ <FormLabel>Receiver Funds</FormLabel>
71
+ <FormControl>
72
+ <Input
73
+ type="text"
74
+ inputMode="decimal"
75
+ placeholder="Enter receiver funds"
76
+ value={field.value as unknown as string}
77
+ onChange={(e) => field.onChange(e.target.value)}
78
+ />
79
+ </FormControl>
80
+ <FormMessage />
81
+ </FormItem>
82
+ )}
83
+ />
84
+ </div>
85
+
86
+ <div className="mt-4 flex justify-between items-center">
87
+ <Button
88
+ type="submit"
89
+ disabled={isSubmitting}
90
+ className="cursor-pointer"
91
+ >
92
+ {isSubmitting ? (
93
+ <div className="flex items-center">
94
+ <Loader2 className="h-5 w-5 animate-spin" />
95
+ <span className="ml-2">Resolving...</span>
96
+ </div>
97
+ ) : (
98
+ "Resolve"
99
+ )}
100
+ </Button>
101
+
102
+ <p className="text-xs text-muted-foreground">
103
+ <span className="font-bold">Total Amount: </span>
104
+ {formatCurrency(
105
+ selectedEscrow?.amount || 0,
106
+ selectedEscrow?.trustline.name || ""
107
+ )}
108
+ </p>
109
+
110
+ <p className="text-xs text-muted-foreground">
111
+ <span className="font-bold">Total Balance: </span>
112
+ {formatCurrency(
113
+ selectedEscrow?.balance || 0,
114
+ selectedEscrow?.trustline.name || ""
115
+ )}
116
+ </p>
117
+ </div>
118
+ </form>
119
+ </Form>
120
+ </DialogContent>
121
+ </Dialog>
122
+ );
123
+ }