diff --git a/components/settings/settings-form.tsx b/components/settings/settings-form.tsx index 690d3ca8..f82b08e6 100644 --- a/components/settings/settings-form.tsx +++ b/components/settings/settings-form.tsx @@ -19,11 +19,13 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; +import revalidatePathServerAction from "@/app/actions"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useAddOrUpdateUser, useGetUser } from "@/users/hooks"; import { useAccountDetails } from "@/hooks/useAccountDetails"; import { useAccountStore } from "@/lib/account-store"; +import { useCancelSignatureRequest } from "@/safe/signature-requests/useCancelSignatureRequest"; import { errorHasMessage } from "@/lib/errorHasMessage"; import { errorHasReason } from "@/lib/errorHasReason"; @@ -31,7 +33,6 @@ import { parsePendingUserUpdate, type PendingUserUpdate, } from "@/settings/pending-user-update-parser"; - const formSchema = z.object({ displayName: z.string().max(30, "Max. 30 characters").optional(), avatar: z.union([z.string().url("Invalid URL"), z.literal("")]).optional(), @@ -107,7 +108,12 @@ export const SettingsForm = () => { } }; - const isPending = isPendingGetUser || isLoadingDetails || isPendingUpdateUser; + const cancelSignatureRequest = useCancelSignatureRequest(); + const isPending = + isPendingGetUser || + isLoadingDetails || + isPendingUpdateUser || + cancelSignatureRequest.isPending; const form = useForm({ resolver: zodResolver(formSchema), @@ -206,6 +212,35 @@ export const SettingsForm = () => { checkPendingUpdates(); }, [selectedAccount, userData]); + const handleCancelUpdate = async () => { + if ( + !selectedAccount?.address || + !userData?.pendingSignatures[0]?.message_hash + ) + return; + + // Prevent any other operations while cancellation is in progress + if (cancelSignatureRequest.isPending) return; + + try { + await cancelSignatureRequest.mutateAsync({ + safeAddress: selectedAccount.address, + messageHash: userData.pendingSignatures[0].message_hash, + }); + setPendingUpdate(undefined); + + // Reset form with current values to make it editable again + form.reset({ + displayName: form.getValues("displayName"), + avatar: form.getValues("avatar"), + }); + + revalidatePathServerAction("/settings"); + } catch (error) { + console.error("Failed to cancel signature request:", error); + } + }; + const submitDisabled = form.formState.isSubmitting || !form.formState.isValid || @@ -272,49 +307,56 @@ export const SettingsForm = () => { )} Save changes - - {pendingUpdate && ( -
-
-

- Pending Update -

- - {formatDistanceToNow( - new Date(pendingUpdate.metadata.timestamp), - { addSuffix: true }, - )} - -
- -
-
- - Display Name - - - {pendingUpdate.user.displayName} - -
- -
- - Image URL - - - {pendingUpdate.user.avatar} - -
-
- -

- The changes will be applied once all required signatures are - collected. -

-
- )} + + {pendingUpdate && ( +
+
+

+ Pending Update +

+ + {formatDistanceToNow(new Date(pendingUpdate.metadata.timestamp), { + addSuffix: true, + })} + +
+ +
+
+ + Display Name + + + {pendingUpdate.user.displayName} + +
+ +
+ + Image URL + + + {pendingUpdate.user.avatar} + +
+
+ +

+ The changes will be applied once all required signatures are + collected. +

+ +
+ )} ); }; diff --git a/safe/signature-requests/useCancelSignatureRequest.ts b/safe/signature-requests/useCancelSignatureRequest.ts new file mode 100644 index 00000000..b94b0127 --- /dev/null +++ b/safe/signature-requests/useCancelSignatureRequest.ts @@ -0,0 +1,96 @@ +import { useMutation } from "@tanstack/react-query"; +import { useAccount } from "wagmi"; + +import { useStepProcessDialogContext } from "@/components/global/step-process-dialog"; +import { HYPERCERTS_API_URL_REST } from "@/configs/hypercerts"; +import { signMessage } from "@/lib/sign-api-message"; +import revalidatePathServerAction from "@/app/actions"; + +const STEP_1 = "step1"; +const STEP_2 = "step2"; + +export const useCancelSignatureRequest = () => { + const { address, chainId } = useAccount(); + const dialogContext = useStepProcessDialogContext(); + + return useMutation({ + mutationKey: ["cancelSignatureRequest"], + mutationFn: async ({ + safeAddress, + messageHash, + }: { + safeAddress: string; + messageHash: string; + }) => { + if (!address || !chainId) { + throw new Error("No address or chainId found"); + } + + const { setDialogStep: setStep, setSteps, setOpen } = dialogContext; + + setSteps([ + { id: STEP_1, description: "Sign cancellation request" }, + { id: STEP_2, description: "Canceling request" }, + ]); + setOpen(true); + + await setStep(STEP_1); + + let signature: string; + try { + signature = await signMessage(address, chainId, { + types: { + SignatureRequest: [ + { name: "cancelSignatureRequestId", type: "string" }, + ], + }, + primaryType: "SignatureRequest", + message: { + cancelSignatureRequestId: `${safeAddress}-${messageHash}`, + }, + }); + } catch (error) { + await setStep( + STEP_1, + "error", + error instanceof Error ? error.message : "Error signing message", + ); + throw error; + } + + await setStep(STEP_2); + + try { + const response = await fetch( + `${HYPERCERTS_API_URL_REST}/signature-requests/${safeAddress}-${messageHash}/cancel`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + signature, + owner_address: address, + chain_id: chainId, + }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to cancel signature request"); + } + + await setStep(STEP_2, "completed"); + await revalidatePathServerAction("/settings"); + setTimeout(() => setOpen(false), 2000); + } catch (error) { + await setStep( + STEP_2, + "error", + error instanceof Error ? error.message : "Error canceling request", + ); + throw error; + } + }, + }); +}; diff --git a/users/hooks.ts b/users/hooks.ts index 01521c31..de7dbe22 100644 --- a/users/hooks.ts +++ b/users/hooks.ts @@ -41,6 +41,8 @@ export const useGetUser = ({ address }: { address?: string }) => { where: { safe_address: { eq: $address }, status: { eq: PENDING } } ) { data { + message_hash + safe_address message } }