From fcf84082dfe43ab7ed4fae35ff29399d8741a8cd Mon Sep 17 00:00:00 2001 From: ogous Date: Fri, 30 Aug 2024 14:27:46 +0300 Subject: [PATCH] feat: error reporting dialog --- web/src/app/layout.tsx | 3 +- web/src/app/stake/MyPositions.tsx | 6 +-- .../app/stake/pool/confirm-withdraw/page.tsx | 6 +-- web/src/app/stake/pool/page.tsx | 5 +-- web/src/components/ConfirmStake.tsx | 10 ++--- web/src/components/ConfirmSwap.tsx | 9 ++-- web/src/components/ErrorReportingDialog.tsx | 40 ++++++++++++++++++ web/src/components/ui/dialog.tsx | 26 ++++++++++++ web/src/fixtures/wagmi/useWriteContract.tsx | 41 +++++++++++++++++++ web/src/stores/useErrorReport.ts | 15 +++++++ 10 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 web/src/components/ErrorReportingDialog.tsx create mode 100644 web/src/components/ui/dialog.tsx create mode 100644 web/src/fixtures/wagmi/useWriteContract.tsx create mode 100644 web/src/stores/useErrorReport.ts diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index b5d147da..bd7fafe8 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -16,7 +16,7 @@ import { graphqlEndpoint } from "@/config/graphqlEndpoint"; import { graphqlQueryGlobal } from "@/hooks/useGraphql"; import PopulateQueryCache from "@/app/PopulateQueryCache"; import BottomBanner from "@/components/Banners/BottomBanner"; - +import ErrorReportingDialog from "@/components/ErrorReportingDialog"; const title = "Longtail"; const description = "Longtail is Arbitrum's cheapest and most rewarding AMM."; @@ -141,6 +141,7 @@ export default async function RootLayout({ + diff --git a/web/src/app/stake/MyPositions.tsx b/web/src/app/stake/MyPositions.tsx index 3949870a..a004fa94 100644 --- a/web/src/app/stake/MyPositions.tsx +++ b/web/src/app/stake/MyPositions.tsx @@ -12,12 +12,12 @@ import { usdFormat } from "@/lib/usdFormat"; import Position from "@/assets/icons/position.svg"; import { output as seawaterContract } from "@/lib/abi/ISeawaterAMM"; import { Button } from "@/components/ui/button"; -import { nanoid } from "nanoid"; import { motion } from "framer-motion"; import Link from "next/link"; import TokenIridescent from "@/assets/icons/token-iridescent.svg"; import SegmentedControl from "@/components/ui/segmented-control"; -import { useAccount, useSimulateContract, useWriteContract } from "wagmi"; +import { useAccount, useSimulateContract } from "wagmi"; +import useWriteContract from "@/fixtures/wagmi/useWriteContract"; import { mockMyPositions } from "@/demoData/myPositions"; import { useFeatureFlag } from "@/hooks/useFeatureFlag"; import { Token, fUSDC, getTokenFromAddress } from "@/config/tokens"; @@ -85,7 +85,7 @@ export const MyPositions = () => { : 0n; const { - writeContract: writeContractCollect, + writeContractAsync: writeContractCollect, data: collectData, error: collectError, isPending: isCollectPending, diff --git a/web/src/app/stake/pool/confirm-withdraw/page.tsx b/web/src/app/stake/pool/confirm-withdraw/page.tsx index c97a193a..b1c31fcd 100644 --- a/web/src/app/stake/pool/confirm-withdraw/page.tsx +++ b/web/src/app/stake/pool/confirm-withdraw/page.tsx @@ -11,8 +11,8 @@ import { useChainId, useSimulateContract, useWaitForTransactionReceipt, - useWriteContract, } from "wagmi"; +import useWriteContract from "@/fixtures/wagmi/useWriteContract"; import { fUSDC } from "@/config/tokens"; import { sqrtPriceX96ToPrice } from "@/lib/math"; import { @@ -62,7 +62,7 @@ export default function ConfirmWithdrawLiquidity() { const isWithdrawingEntirePosition = positionLiquidity?.result === delta; const { - writeContract: writeContractUpdatePosition, + writeContractAsync: writeContractUpdatePosition, data: updatePositionData, error: updatePositionError, isPending: isUpdatePositionPending, @@ -74,7 +74,7 @@ export default function ConfirmWithdrawLiquidity() { }); const { - writeContract: writeContractCollect, + writeContractAsync: writeContractCollect, data: collectData, error: collectError, isPending: isCollectPending, diff --git a/web/src/app/stake/pool/page.tsx b/web/src/app/stake/pool/page.tsx index b225a2a1..111cb0c7 100644 --- a/web/src/app/stake/pool/page.tsx +++ b/web/src/app/stake/pool/page.tsx @@ -8,8 +8,6 @@ import Token from "@/assets/icons/token.svg"; import { Badge } from "@/components/ui/badge"; import { Line } from "rc-progress"; import { motion } from "framer-motion"; -import { format, subDays } from "date-fns"; -import ReactECharts from "echarts-for-react"; import Link from "next/link"; import { output as seawaterContract } from "@/lib/abi/ISeawaterAMM"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -29,7 +27,8 @@ import { getFormattedPriceFromTick } from "@/lib/amounts"; import { useStakeStore } from "@/stores/useStakeStore"; import { useSwapStore } from "@/stores/useSwapStore"; import { ammAddress } from "@/lib/addresses"; -import { useSimulateContract, useWriteContract } from "wagmi"; +import { useSimulateContract } from "wagmi"; +import useWriteContract from "@/fixtures/wagmi/useWriteContract"; import { getSqrtRatioAtTick, getTokenAmountsNumeric, diff --git a/web/src/components/ConfirmStake.tsx b/web/src/components/ConfirmStake.tsx index 24df7055..4a8c642a 100644 --- a/web/src/components/ConfirmStake.tsx +++ b/web/src/components/ConfirmStake.tsx @@ -11,8 +11,8 @@ import { useChainId, useSimulateContract, useWaitForTransactionReceipt, - useWriteContract, } from "wagmi"; +import useWriteContract from "@/fixtures/wagmi/useWriteContract"; import { output as seawaterContract } from "@/lib/abi/ISeawaterAMM"; import { sqrtPriceX96ToPrice, @@ -129,27 +129,27 @@ export const ConfirmStake = ({ mode, positionId }: ConfirmStakeProps) => { // set up write contract hooks const { - writeContract: writeContractMint, + writeContractAsync: writeContractMint, data: mintData, error: mintError, isPending: isMintPending, } = useWriteContract(); const { - writeContract: writeContractApprovalToken0, + writeContractAsync: writeContractApprovalToken0, data: approvalDataToken0, error: approvalErrorToken0, isPending: isApprovalPendingToken0, reset: resetApproveToken0, } = useWriteContract(); const { - writeContract: writeContractApprovalToken1, + writeContractAsync: writeContractApprovalToken1, data: approvalDataToken1, error: approvalErrorToken1, isPending: isApprovalPendingToken1, reset: resetApproveToken1, } = useWriteContract(); const { - writeContract: writeContractUpdatePosition, + writeContractAsync: writeContractUpdatePosition, data: updatePositionData, error: updatePositionError, isPending: isUpdatePositionPending, diff --git a/web/src/components/ConfirmSwap.tsx b/web/src/components/ConfirmSwap.tsx index f75e5dce..119c6c41 100644 --- a/web/src/components/ConfirmSwap.tsx +++ b/web/src/components/ConfirmSwap.tsx @@ -10,8 +10,8 @@ import { useChainId, useSimulateContract, useWaitForTransactionReceipt, - useWriteContract, } from "wagmi"; +import useWriteContract from "@/fixtures/wagmi/useWriteContract"; import { output as seawaterContract } from "@/lib/abi/ISeawaterAMM"; import { sqrtPriceX96ToPrice } from "@/lib/math"; import { useEffect, useCallback, useMemo } from "react"; @@ -109,14 +109,14 @@ export const ConfirmSwap = () => { // set up write hooks const { - writeContract: writeContractApproval, + writeContractAsync: writeContractApproval, data: approvalData, error: approvalError, isPending: isApprovalPending, reset: resetApproval, } = useWriteContract(); const { - writeContract: writeContractSwap, + writeContractAsync: writeContractSwap, data: swapData, error: swapError, isPending: isSwapPending, @@ -207,9 +207,6 @@ export const ConfirmSwap = () => { const performSwap = useCallback(() => { writeContractSwap({ ...swapOptions, - // Typescript doesn't support strongly typing this with destructuring - // https://github.com/microsoft/TypeScript/issues/46680 - // @ts-expect-error args: swapOptions.args, }); }, [swapOptions, writeContractSwap]); diff --git a/web/src/components/ErrorReportingDialog.tsx b/web/src/components/ErrorReportingDialog.tsx new file mode 100644 index 00000000..7faa095d --- /dev/null +++ b/web/src/components/ErrorReportingDialog.tsx @@ -0,0 +1,40 @@ +"use client"; +import React from "react"; +import { useErrorReportingStore } from "../stores/useErrorReport"; +import { Dialog, DialogContent } from "./ui/dialog"; +import { Button } from "./ui/button"; +import * as Sentry from "@sentry/nextjs"; + +export default function ErrorReportingDialog() { + const { isOpen, setIsOpen, error, setError } = useErrorReportingStore(); + async function handleError() { + Sentry.captureException(error); + // close dialog + setError(null); + setIsOpen(false); + } + return ( + { + setIsOpen(isOpen); + !isOpen && setError(null); + }} + > + +
+ Report this error? + + {error instanceof Error ? error.name : "Unknown error name"} + +

+ {error instanceof Error ? error.message : "Unknown error message"} +

+ +
+
+
+ ); +} diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx new file mode 100644 index 00000000..82a726e5 --- /dev/null +++ b/web/src/components/ui/dialog.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; + +export const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, ...props }, forwardedRef) => ( + + + +
+ + X + +
+ {children} +
+
+)); + +export const Dialog = DialogPrimitive.Root; +export const DialogTrigger = DialogPrimitive.Trigger; +DialogContent.displayName = DialogPrimitive.Content.displayName; diff --git a/web/src/fixtures/wagmi/useWriteContract.tsx b/web/src/fixtures/wagmi/useWriteContract.tsx new file mode 100644 index 00000000..d6469480 --- /dev/null +++ b/web/src/fixtures/wagmi/useWriteContract.tsx @@ -0,0 +1,41 @@ +import { useWriteContract as baseUseWriteContract, Config } from "wagmi"; +import { WriteContractMutateAsync } from "wagmi/query"; +import { useErrorReportingStore } from "@/stores/useErrorReport"; + +type VariablesType = T extends (variables: infer V, ...args: any[]) => void + ? V + : never; + +export default function useWriteContract() { + const { + writeContractAsync: baseWriteContractAsync, + writeContract, + ...props + } = baseUseWriteContract(); + const setIsOpen = useErrorReportingStore((s) => s.setIsOpen); + const setError = useErrorReportingStore((s) => s.setError); + + function handleError(error: unknown) { + if (error instanceof Error && error.message.includes("User rejected")) + return; + setError(error); + setIsOpen(true); + } + + async function writeContractAsync( + props: VariablesType>, + ) { + try { + return await baseWriteContractAsync(props); + } catch (error) { + handleError(error); + } + } + + return { + ...props, + // do not export a sync write to be able to handle error + // writeContract, + writeContractAsync, + }; +} diff --git a/web/src/stores/useErrorReport.ts b/web/src/stores/useErrorReport.ts new file mode 100644 index 00000000..d40c8c70 --- /dev/null +++ b/web/src/stores/useErrorReport.ts @@ -0,0 +1,15 @@ +import { create } from "zustand"; + +interface ErrorReportingStore { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + error: unknown | null; + setError: (error: unknown) => void; +} + +export const useErrorReportingStore = create((set) => ({ + isOpen: false, + setIsOpen: (isOpen) => set({ isOpen }), + error: null, + setError: (error: unknown) => set({ error }), +}));