diff --git a/.eslintrc.json b/.eslintrc.json index 03cf4589d..62c4f30a8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -85,8 +85,13 @@ { "selector": "typeLike", "format": ["PascalCase"] + }, + { + "selector": "typeParameter", + "format": ["PascalCase", "camelCase"] } ], + "@typescript-eslint/no-redeclare": "off", "radix": "off", "consistent-return": "off", "jsx-a11y/anchor-is-valid": "off", diff --git a/.github/workflows/knip.yaml b/.github/workflows/knip.yaml index 50fe208ac..4d2aa9e18 100644 --- a/.github/workflows/knip.yaml +++ b/.github/workflows/knip.yaml @@ -1,6 +1,6 @@ name: Knip -on: [push] +on: [] jobs: knip: diff --git a/.github/workflows/pages-deployment.yaml b/.github/workflows/pages-deployment.yaml index b96c3d2e4..c72757a14 100644 --- a/.github/workflows/pages-deployment.yaml +++ b/.github/workflows/pages-deployment.yaml @@ -4,7 +4,7 @@ env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_PUBLIC_INTERCOM_ID: re9q5yti -on: [push] +on: [] jobs: yalc_check: runs-on: ubuntu-latest diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ac27c8c28..fdc0f36bf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,6 +1,6 @@ name: Test -on: [push] +on: [] env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} @@ -97,7 +97,38 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] + shard: + [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + ] steps: - uses: actions/checkout@v3 diff --git a/package.json b/package.json index 4067cf45b..927f44db8 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "i18next-browser-languagedetector": "^6.1.5", "i18next-http-backend": "^1.4.1", "idb-keyval": "^6.2.1", - "immer": "^9.0.15", + "immer": "^9.0.21", "intl-segmenter-polyfill": "^0.4.4", "iso-639-1": "^2.1.15", "lodash": "^4.17.21", @@ -97,7 +97,8 @@ "ts-pattern": "^4.2.2", "use-immer": "^0.7.0", "viem": "2.19.4", - "wagmi": "2.12.4" + "wagmi": "2.12.4", + "zustand": "5.0.0-rc.2" }, "peerDependencies": { "react": "*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 820b63458..9d6baef39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,7 +108,7 @@ importers: specifier: ^6.2.1 version: 6.2.1 immer: - specifier: ^9.0.15 + specifier: ^9.0.21 version: 9.0.21 intl-segmenter-polyfill: specifier: ^0.4.4 @@ -173,6 +173,9 @@ importers: wagmi: specifier: 2.12.4 version: 2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + zustand: + specifier: 5.0.0-rc.2 + version: 5.0.0-rc.2(@types/react@18.2.21)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.2.0(react@18.3.1)) devDependencies: '@adraffy/ens-normalize': specifier: ^1.10.1 @@ -10426,6 +10429,24 @@ packages: react: optional: true + zustand@5.0.0-rc.2: + resolution: {integrity: sha512-o2Nwuvnk8vQBX7CcHL8WfFkZNJdxB/VKeWw0tNglw8p4cypsZ3tRT7rTRTDNeUPFS0qaMBRSKe+fVwL5xpcE3A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: ^18.2.0 + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@adobe/css-tools@4.3.3': {} @@ -22967,3 +22988,10 @@ snapshots: '@types/react': 18.2.21 immer: 9.0.21 react: 18.3.1 + + zustand@5.0.0-rc.2(@types/react@18.2.21)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.2.0(react@18.3.1)): + optionalDependencies: + '@types/react': 18.2.21 + immer: 9.0.21 + react: 18.3.1 + use-sync-external-store: 1.2.0(react@18.3.1) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index e2b0f4c2a..986eff0ed 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -282,12 +282,12 @@ "pending": { "regular": "Pending" }, - "confirmed": { + "success": { "regular": "Confirmed", "notifyTitle": "Transaction Successful", "notifyMessage": "Your \"{{action}}\" transaction was successful" }, - "failed": { + "reverted": { "regular": "Failed", "notifyTitle": "Transaction Failure", "notifyMessage": "Your \"{{action}}\" transaction failed and was reverted" @@ -305,7 +305,7 @@ "waitingForWallet": "Waiting for Wallet", "openWallet": "Open Wallet" }, - "sent": { + "pending": { "title": "Transaction Sent", "message": "Your transaction is now in progress, you can close this and come back later.", "progress": { @@ -314,14 +314,14 @@ }, "learn": "Learn about long running transactions" }, - "complete": { + "success": { "title": "Transaction Complete", "message": "Your transaction is now complete!", "progress": { "title": "Done" } }, - "failed": { + "reverted": { "title": "Transaction Failed", "progress": { "title": "Failed" diff --git a/src/assets/verification/DynamicVerificationIcon.tsx b/src/assets/verification/DynamicVerificationIcon.tsx index 653cdf10a..803e318de 100644 --- a/src/assets/verification/DynamicVerificationIcon.tsx +++ b/src/assets/verification/DynamicVerificationIcon.tsx @@ -1,6 +1,6 @@ import dynamic from 'next/dynamic' -import { VerificationProtocol } from '../../transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' export const verificationIconTypes: { [key in VerificationProtocol]: any diff --git a/src/components/@molecules/DogFood.tsx b/src/components/@molecules/DogFood.tsx index e3c2505eb..9207f883b 100644 --- a/src/components/@molecules/DogFood.tsx +++ b/src/components/@molecules/DogFood.tsx @@ -12,8 +12,7 @@ import { Spacer } from '@app/components/@atoms/Spacer' import { useAddressRecord } from '@app/hooks/ensjs/public/useAddressRecord' import useDebouncedCallback from '@app/hooks/useDebouncedCallback' import { createQueryKey } from '@app/hooks/useQueryOptions' - -import { DisplayItems } from './TransactionDialogManager/DisplayItems' +import { DisplayItems } from '@app/transaction/components/DisplayItems' const InnerContainer = styled.div(() => [ css` diff --git a/src/components/@molecules/EditResolver/EditResolverForm.tsx b/src/components/@molecules/EditResolver/EditResolverForm.tsx index c7a6f37d2..b5372cd88 100644 --- a/src/components/@molecules/EditResolver/EditResolverForm.tsx +++ b/src/components/@molecules/EditResolver/EditResolverForm.tsx @@ -1,13 +1,13 @@ import { RefObject } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { useChainId } from 'wagmi' import { Dialog, RadioButton, Typography } from '@ensdomains/thorin' import { Outlink } from '@app/components/Outlink' -import { useChainName } from '@app/hooks/chain/useChainName' import useResolverEditor from '@app/hooks/useResolverEditor' -import { makeEtherscanLink } from '@app/utils/utils' +import { createEtherscanLink } from '@app/utils/utils' import { DogFood } from '../DogFood' import EditResolverWarnings from './EditResolverWarnings' @@ -48,7 +48,7 @@ type Props = ReturnType & { const EditResolverForm = ({ isResolverAddressLatest, - lastestResolverAddress, + latestResolverAddress, resolverChoice, handleSubmit, register, @@ -63,7 +63,7 @@ const EditResolverForm = ({ resolverWarnings, }: Props) => { const { t } = useTranslation('transactionFlow') - const chainName = useChainName() + const chainId = useChainId() const latestResolverLabel = ( @@ -71,7 +71,7 @@ const EditResolverForm = ({ {t('input.editResolver.latestLabel')} {t('input.editResolver.etherscan')} diff --git a/src/components/@molecules/FaucetBanner.tsx b/src/components/@molecules/FaucetBanner.tsx index f6f9a3646..afccf2d56 100644 --- a/src/components/@molecules/FaucetBanner.tsx +++ b/src/components/@molecules/FaucetBanner.tsx @@ -20,8 +20,7 @@ import { import { useAccountSafely } from '@app/hooks/account/useAccountSafely' import { useChainName } from '@app/hooks/chain/useChainName' import useFaucet from '@app/hooks/useFaucet' - -import { DisplayItems } from './TransactionDialogManager/DisplayItems' +import { DisplayItems } from '@app/transaction/components/DisplayItems' const BannerWrapper = styled.div( () => css` @@ -72,8 +71,7 @@ const FaucetBanner = () => { closeDialog() }, [chainName, address]) - if ((chainName !== 'goerli' && chainName !== 'sepolia') || !isReady || isLoading || !data) - return null + if (chainName !== 'sepolia' || !isReady || isLoading || !data) return null const BannerComponent = ( diff --git a/src/components/@molecules/NameListView/NameListView.tsx b/src/components/@molecules/NameListView/NameListView.tsx index 0aae5fede..f91b52103 100644 --- a/src/components/@molecules/NameListView/NameListView.tsx +++ b/src/components/@molecules/NameListView/NameListView.tsx @@ -21,7 +21,8 @@ import { usePrefetchBlockTimestamp } from '@app/hooks/chain/useBlockTimestamp' import { useNamesForAddress } from '@app/hooks/ensjs/subgraph/useNamesForAddress' import useDebouncedCallback from '@app/hooks/useDebouncedCallback' import { useQueryParameterState } from '@app/hooks/useQueryParameterState' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { useTransactionManager } from '@app/transaction/transactionManager' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' const EmptyDetailContainer = styled.div( ({ theme }) => css` @@ -120,7 +121,7 @@ export const NameListView = ({ address, selfAddress, setError, setLoading }: Nam // eslint-disable-next-line react-hooks/exhaustive-deps }, [isNamesLoading]) - const { usePreparedDataInput, getTransactionFlowStage } = useTransactionFlow() + const getTransactionFlowStage = useTransactionManager((s) => s.getFlowStageOrNull) const showExtendNamesInput = usePreparedDataInput('ExtendNames') const [isIntersecting, setIsIntersecting] = useState(false) @@ -142,7 +143,7 @@ export const NameListView = ({ address, selfAddress, setError, setLoading }: Nam const stage = getTransactionFlowStage(`extend-names-${selectedNames.join('-')}`) useEffect(() => { - if (stage === 'completed') { + if (stage === 'complete') { setSelectedNames([]) setMode('view') } diff --git a/src/components/@molecules/TransactionDialogManager/DisplayItems.test.tsx b/src/components/@molecules/TransactionDialogManager/DisplayItems.test.tsx deleted file mode 100644 index b31444895..000000000 --- a/src/components/@molecules/TransactionDialogManager/DisplayItems.test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { mockFunction, render, screen } from '@app/test-utils' - -import { describe, expect, it, vi } from 'vitest' - -import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' -import { TransactionDisplayItem } from '@app/types' - -import { DisplayItems } from './DisplayItems' - -vi.mock('@app/hooks/ensjs/public/usePrimaryName') - -const mockUsePrimaryName = mockFunction(usePrimaryName) - -const genericItem: TransactionDisplayItem = { - label: 'GenericItem', - value: 'GenericValue', -} - -const addressItem: TransactionDisplayItem = { - label: 'AddressItem', - value: '0x1234567890123456789012345678901234567890', - type: 'address', -} - -const nameItem: TransactionDisplayItem = { - label: 'NameItem', - value: 'test.eth', - type: 'name', -} - -describe('DisplayItems', () => { - it('should show a generic item', () => { - render() - expect(screen.getByText('transaction.itemLabel.GenericItem')).toBeVisible() - expect(screen.getByText('GenericValue')).toBeVisible() - }) - it('should show the raw label', () => { - render() - expect(screen.getByText('GenericItem')).toBeVisible() - expect(screen.getByText('GenericValue')).toBeVisible() - }) - it('should show an address item and primary name', () => { - mockUsePrimaryName.mockReturnValue({ - data: { - name: 'test.eth', - beautifiedName: 'test.eth', - }, - isLoading: false, - status: 'success', - }) - render() - expect(screen.getByText('transaction.itemLabel.AddressItem')).toBeVisible() - expect(screen.getByText('0x123...67890')).toBeVisible() - expect(screen.getByText('test.eth')).toBeVisible() - }) - it('should show an address item and no primary name', () => { - mockUsePrimaryName.mockReturnValue({ - data: { name: undefined, beautifiedName: undefined }, - isLoading: false, - status: 'success', - }) - render() - expect(screen.getByText('transaction.itemLabel.AddressItem')).toBeVisible() - expect(screen.getByText('0x123...67890')).toBeVisible() - expect(screen.queryByText('test.eth')).not.toBeInTheDocument() - }) - it('should show a name item', () => { - render() - expect(screen.getByText('transaction.itemLabel.NameItem')).toBeVisible() - expect(screen.getByText('test.eth')).toBeVisible() - }) - it('should render multiple items', () => { - mockUsePrimaryName.mockReturnValue({ - data: { name: undefined, beautifiedName: undefined }, - isLoading: false, - status: 'success', - }) - render() - expect(screen.getByText('transaction.itemLabel.AddressItem')).toBeVisible() - expect(screen.getByText('0x123...67890')).toBeVisible() - expect(screen.getByText('transaction.itemLabel.NameItem')).toBeVisible() - expect(screen.getByText('test.eth')).toBeVisible() - expect(screen.getByText('transaction.itemLabel.GenericItem')).toBeVisible() - expect(screen.getByText('GenericValue')).toBeVisible() - }) -}) diff --git a/src/components/@molecules/TransactionDialogManager/InputComponentWrapper.test.tsx b/src/components/@molecules/TransactionDialogManager/InputComponentWrapper.test.tsx deleted file mode 100644 index dfaa985ea..000000000 --- a/src/components/@molecules/TransactionDialogManager/InputComponentWrapper.test.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import { act, render, screen, waitFor } from '@app/test-utils' - -import { QueryClientProvider } from '@tanstack/react-query' -import { useQuery } from '@app/utils/query/useQuery' - -import { ReactNode, useContext, useEffect } from 'react' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { WagmiProvider } from 'wagmi' - -import { queryClientWithRefetch as queryClient } from '@app/utils/query/reactQuery' -import { wagmiConfig } from '@app/utils/query/wagmi' - -import DynamicLoadingContext from './DynamicLoadingContext' -import InputComponentWrapper from './InputComponentWrapper' - -const cache = queryClient.getQueryCache() -queryClient.setDefaultOptions({ - queries: { - refetchOnWindowFocus: true, - refetchInterval: 1000 * 60, - staleTime: 1000 * 120, - meta: { - isRefetchQuery: true, - }, - refetchOnMount: 'always', - }, -}) - -const ComponentHelper = ({ children }: { children: ReactNode }) => { - return ( -
-
- - - {children} - - -
-
- ) -} - -const mockObserve = vi.fn() -const mockDisconnect = vi.fn() - -const ComponentWithHook = ({ timeout }: { timeout: number }) => { - useQuery({ - queryKey: ['test', '123'], - queryFn: () => - new Promise((resolve) => { - setTimeout(() => resolve('value-updated'), timeout) - }), - }) - return
-} - -const ComponentLoading = () => { - const setLoading = useContext(DynamicLoadingContext) - useEffect(() => { - setLoading(true) - return () => setLoading(false) - }, [setLoading]) - - return
-} - -describe('', () => { - let mutationObserverCb: () => void - beforeEach(() => { - ;(global.MutationObserver as any) = class { - constructor(cb: any) { - mutationObserverCb = cb - } - - observe = mockObserve - - disconnect = mockDisconnect - } - }) - it('should render children', () => { - render( - -
- , - ) - expect(screen.getByTestId('test')).toBeVisible() - }) - it('should set all queries with no observers to idle', () => { - queryClient.setQueryData(['test', '123'], 'value') - queryClient.setQueryData(['test', '456'], 'value') - const item1 = cache.get('["test","123"]')! - const item2 = cache.get('["test","456"]')! - item1.setState({ ...item1.state, fetchStatus: 'fetching' }) - item2.setState({ ...item2.state, fetchStatus: 'fetching' }) - render( - -
- , - ) - expect(item1.state.fetchStatus).toBe('idle') - expect(item2.state.fetchStatus).toBe('idle') - }) - it('should add cacheable-component class to modal card on mount', async () => { - render( - -
- , - ) - mutationObserverCb() - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component') - }) - }) - it('should add cacheable-component-cached class to modal card when cached data exists', async () => { - queryClient.setQueryData(['test', '123'], 'value') - render( - - - , - ) - mutationObserverCb() - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component-cached') - }) - }) - it('should show spinner after data is cached for 3 seconds', async () => { - vi.useFakeTimers() - queryClient.setQueryData(['test', '123'], 'value') - render( - - - , - ) - mutationObserverCb() - act(() => { - vi.advanceTimersByTime(3000) - }) - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component-cached') - expect(screen.getByTestId('spinner-overlay')).toBeVisible() - }) - }) - it('should not show spinner if componentLoading is true', async () => { - vi.useFakeTimers() - render( - - - , - ) - mutationObserverCb() - act(() => { - vi.advanceTimersByTime(3000) - }) - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component') - expect(screen.queryByTestId('spinner-overlay')).toBeNull() - }) - }) - it('should remove cacheable-component-cached class from modal once data is refetched', async () => { - vi.useFakeTimers() - queryClient.setQueryData(['test', '123'], 'value') - render( - - - , - ) - mutationObserverCb() - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component-cached') - }) - act(() => { - vi.advanceTimersByTime(3000) - }) - await waitFor(() => { - expect(screen.getByTestId('spinner-overlay')).toBeVisible() - }) - act(() => { - vi.advanceTimersByTime(2000) - }) - await waitFor(() => { - expect(screen.getByTestId('modal-card')).not.toHaveClass('cacheable-component-cached') - expect(screen.queryByTestId('spinner-overlay')).toBeNull() - }) - }) - it('should remove cacheable-component class from modal card on unmount', async () => { - render(
) - const { unmount } = render( - - - -
- - - , - ) - mutationObserverCb() - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component') - }) - unmount() - await waitFor(() => { - expect(screen.getByTestId('modal-card')).not.toHaveClass('cacheable-component') - }) - }) - it('should not initially wait for queries to be fetched if there are no queries', async () => { - render( - -
- , - ) - mutationObserverCb() - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component') - expect(screen.getByTestId('modal-card')).not.toHaveClass('cacheable-component-cached') - }) - }) - it('should add cacheable-component-cached class if there are stale queries', async () => { - vi.useFakeTimers() - render( - - - , - ) - mutationObserverCb() - act(() => { - vi.advanceTimersByTime(100) - }) - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component') - expect(screen.getByTestId('modal-card')).not.toHaveClass('cacheable-component-cached') - }) - const item1 = cache.get('["test","123"]')! - act(() => { - item1.setState({ ...item1.state, dataUpdatedAt: Date.now() - 1000 * 240 }) - vi.advanceTimersByTime(5000) - }) - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component-cached') - }) - }) - it('should remove cacheable-component-cached class once stale queries are refetched', async () => { - vi.useFakeTimers() - render( - - - , - ) - mutationObserverCb() - act(() => { - vi.advanceTimersByTime(100) - }) - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component') - expect(screen.getByTestId('modal-card')).not.toHaveClass('cacheable-component-cached') - }) - const item1 = cache.get('["test","123"]')! - act(() => { - item1.setState({ ...item1.state, dataUpdatedAt: Date.now() - 1000 * 240 }) - vi.advanceTimersByTime(5000) - }) - await waitFor(() => { - expect(screen.getByTestId('modal-card')).toHaveClass('cacheable-component-cached') - }) - act(() => { - // remaining time for refetch interval - vi.advanceTimersByTime(1000 * 55) - }) - await waitFor(() => { - expect(screen.getByTestId('modal-card')).not.toHaveClass('cacheable-component-cached') - }) - }) -}) diff --git a/src/components/@molecules/TransactionDialogManager/InputComponentWrapper.tsx b/src/components/@molecules/TransactionDialogManager/InputComponentWrapper.tsx deleted file mode 100644 index a21121a10..000000000 --- a/src/components/@molecules/TransactionDialogManager/InputComponentWrapper.tsx +++ /dev/null @@ -1,226 +0,0 @@ -/* eslint-disable default-case */ - -/* eslint-disable no-param-reassign */ -import { useQueryClient } from '@tanstack/react-query' -import { ReactNode, useEffect, useRef, useState } from 'react' -import styled, { css } from 'styled-components' - -import { Spinner } from '@ensdomains/thorin' - -import DynamicLoadingContext from './DynamicLoadingContext' - -const SpinnerOverlay = styled.div( - () => css` - z-index: 1; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - `, -) - -const getModalCard = () => document.querySelector('.modal') - -const InputComponentWrapper = ({ children }: { children: ReactNode }) => { - const queryClient = useQueryClient() - - const [isCached, _setIsCached] = useState(false) - const [componentLoading, setComponentLoading] = useState(false) - const [showSpinner, setShowSpinner] = useState(false) - - const cachedRef = useRef(false) - - const setIsCached = (cached: boolean) => { - _setIsCached(cached) - cachedRef.current = cached - } - - useEffect(() => { - const cache = queryClient.getQueryCache() - const externalQueries = cache - .getAll() - .filter((q) => q.state.fetchStatus === 'fetching' && !q.getObserversCount()) - - if (externalQueries.length > 0) { - for (const eq of externalQueries) { - eq.setState({ ...eq.state, fetchStatus: 'idle' }) - } - } - }, [queryClient]) - - // hook for detecting when all queries have been refetched on mount generically - // also handles stale queries - useEffect(() => { - let staleCheckInterval: NodeJS.Timeout | undefined - // this can be either the first cache subscription OR the stale check interval subscription - let unsubscribe: (() => void) | undefined - - // if the component is loading, don't do anything - if (!componentLoading) { - const cache = queryClient.getQueryCache() - - const makeStaleCheckInterval = () => { - clearInterval(staleCheckInterval) - // poll for stale queries - staleCheckInterval = setInterval(() => { - // queries must be: - // refetch queries (under the input component) - // enabled - // active - // stale - // and have been updated more than staleTime ago (isStale() doesn't always work for some reason) - const staleQueries = cache.getAll().filter((q) => { - const { enabled } = q.options as any - return ( - q.meta?.isRefetchQuery && - (typeof enabled === 'undefined' || enabled) && - q.isActive() && - q.isStale() && - Date.now() > - q.state.dataUpdatedAt + queryClient.getDefaultOptions().queries!.staleTime! - ) - }) - // if there are stale queries, stop polling, set isCached to true, and subscribe to the cache - if (staleQueries.length > 0) { - clearInterval(staleCheckInterval) - setIsCached(true) - unsubscribe = cache.subscribe((query) => { - // only care about updated queries - if (query.type === 'updated') { - const staleQueryIndex = staleQueries.findIndex( - (q) => q?.queryHash === query.query.queryHash, - ) - const queryState = query.query.state - if ( - staleQueryIndex !== -1 && - queryState.fetchStatus === 'idle' && - // assume that errored queries are handled by the component - (queryState.status === 'success' || queryState.status === 'error') - ) { - // if stale query exists in staleQueries and is updated, delete it from staleQueries - delete staleQueries[staleQueryIndex] - // if all stale queries have been updated, set isCached to false, unsubscribe from cache, and start polling again - if (staleQueries.every((q) => q === undefined)) { - setIsCached(false) - unsubscribe!() - makeStaleCheckInterval() - } - } - } - }) - } - }, 5000) // poll every 5 seconds - } - - const getFetchingQueries = () => - cache.getAll().filter((q) => q.state.fetchStatus === 'fetching' && q.meta?.isRefetchQuery) - const fetchedKeys: string[] = [] - - // if there are any queries to fetch, run the cache subscription - if (getFetchingQueries().length !== 0) { - setIsCached(true) - unsubscribe = cache.subscribe((query) => { - // only care about updated queries - if (query.type === 'updated') { - const queryState = query.query.state - if ( - queryState.fetchStatus === 'idle' && - // assume that errored queries are handled by the component - (queryState.status === 'success' || queryState.status === 'error') - ) { - // if query is updated, add it to fetchedKeys - fetchedKeys.push(query.query.queryHash) - // if all queries are updated, set isCached to false, unsubscribe from cache, and start polling for stale queries - const stillToFetch = getFetchingQueries() - if (stillToFetch.length === 0) { - setIsCached(false) - unsubscribe!() - makeStaleCheckInterval() - } - } - } - }) - } else { - // if there are no queries, assume there is no initial data needed and start polling for stale queries - setIsCached(false) - makeStaleCheckInterval() - } - } - - return () => { - clearInterval(staleCheckInterval) - unsubscribe?.() - } - }, [componentLoading, queryClient]) - - // hook for detecting when the modal card is mounted - // and adding the cacheable-component class to it - // this is needed because of the way the modal is rendered - useEffect(() => { - const observer = new MutationObserver(() => { - const element = getModalCard() - if (element) { - element.classList.add('cacheable-component') - if (cachedRef.current) { - element.classList.add('cacheable-component-cached') - } - observer.disconnect() - } - }) - observer.observe(document.body, { - childList: true, - subtree: true, - }) - return () => { - observer.disconnect() - const modalCard = getModalCard() - modalCard?.classList.remove('cacheable-component') - } - }, []) - - useEffect(() => { - const modalCard = getModalCard() - if (isCached) { - modalCard?.classList.add('cacheable-component-cached') - } else { - modalCard?.classList.remove('cacheable-component-cached') - } - return () => { - modalCard?.classList.remove('cacheable-component-cached') - } - }, [isCached]) - - // hook for showing the spinner after 3 seconds - // uses isMounted to prevent the spinner from showing up on top of the TransactionLoader spinner - useEffect(() => { - let timeout: NodeJS.Timeout | undefined - if (isCached && !componentLoading) { - timeout = setTimeout(() => { - setShowSpinner(true) - }, 3000) - } else { - clearTimeout(timeout) - setShowSpinner(false) - } - return () => { - clearTimeout(timeout) - } - }, [componentLoading, isCached]) - - return ( - - {showSpinner && ( - - - - )} - {children} - - ) -} - -export default InputComponentWrapper diff --git a/src/components/@molecules/TransactionDialogManager/TransactionDialogManager.test.tsx b/src/components/@molecules/TransactionDialogManager/TransactionDialogManager.test.tsx deleted file mode 100644 index 724b5efa1..000000000 --- a/src/components/@molecules/TransactionDialogManager/TransactionDialogManager.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { mockFunction, renderHook } from '@app/test-utils' - -import { describe, expect, it, vi } from 'vitest' -import { useAccount } from 'wagmi' - -import { useResetSelectedKey } from './TransactionDialogManager' - -vi.mock('wagmi') - -const mockUseAccount = mockFunction(useAccount) - -describe('useResetSelectedKey', () => { - it('should stopFlow if account changes', async () => { - const dispatch = vi.fn() - mockUseAccount.mockReturnValue({ address: '0xAddress' }) - const { rerender } = renderHook(() => useResetSelectedKey(dispatch)) - mockUseAccount.mockReturnValue({ address: '0xOtherAddress' }) - rerender() - expect(dispatch).toHaveBeenCalledWith({ - name: 'stopFlow', - }) - }) - - it('should not call stopFlow if account stays the same', () => { - const dispatch = vi.fn() - mockUseAccount.mockReturnValue({ address: '0xAddress' }) - const { rerender } = renderHook(() => useResetSelectedKey(dispatch)) - mockUseAccount.mockReturnValue({ address: '0xAddress' }) - rerender() - expect(dispatch).not.toHaveBeenCalled() - }) -}) diff --git a/src/components/@molecules/TransactionDialogManager/TransactionDialogManager.tsx b/src/components/@molecules/TransactionDialogManager/TransactionDialogManager.tsx deleted file mode 100644 index 5ba1ea14e..000000000 --- a/src/components/@molecules/TransactionDialogManager/TransactionDialogManager.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { QueryClientProvider } from '@tanstack/react-query' -import { Dispatch, useCallback, useEffect, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import usePrevious from 'react-use/lib/usePrevious' -import { match, P } from 'ts-pattern' -import { useAccount, useChainId } from 'wagmi' - -import { Dialog } from '@ensdomains/thorin' - -import { transactions } from '@app/transaction-flow/transaction' -import { queryClientWithRefetch } from '@app/utils/query/reactQuery' - -import { DataInputComponents } from '../../../transaction-flow/input' -import { InternalTransactionFlow, TransactionFlowAction } from '../../../transaction-flow/types' -import { IntroStageModal } from './stage/Intro' -import { TransactionStageModal } from './stage/TransactionStageModal' - -export const useResetSelectedKey = (dispatch: any) => { - const { address } = useAccount() - const chainId = useChainId() - - const prevAddress = usePrevious(address) - const prevChainId = usePrevious(chainId) - - useEffect(() => { - if (prevChainId && prevChainId !== chainId) { - dispatch({ - name: 'stopFlow', - }) - } - }, [prevChainId, chainId, dispatch]) - - useEffect(() => { - if (prevAddress && prevAddress !== address) { - dispatch({ - name: 'stopFlow', - }) - } - }, [prevAddress, address, dispatch]) -} - -export const TransactionDialogManager = ({ - state, - dispatch, - selectedKey, -}: { - state: InternalTransactionFlow - dispatch: Dispatch - selectedKey: string | null -}) => { - const { t } = useTranslation() - const selectedItem = useMemo( - () => (selectedKey ? state.items[selectedKey] : null), - [selectedKey, state.items], - ) - - useResetSelectedKey(dispatch) - - const onDismiss = useCallback(() => { - dispatch({ name: 'stopFlow' }) - }, [dispatch]) - - const onDismissDialog = useCallback(() => { - if (selectedItem?.disableBackgroundClick && selectedItem?.currentFlowStage === 'input') return - dispatch({ - name: 'stopFlow', - }) - }, [dispatch, selectedItem?.disableBackgroundClick, selectedItem?.currentFlowStage]) - - return ( - - {match([selectedKey, selectedItem]) - .with( - [P.not(P.nullish), { input: P.not(P.nullish), currentFlowStage: 'input' }], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_, _selectedItem]) => { - const Component = DataInputComponents[_selectedItem.input.name] - return ( - - - - ) - }, - ) - .with( - [P.not(P.nullish), { intro: P.not(P.nullish), currentFlowStage: 'intro' }], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_, _selectedItem]) => { - const currentTx = _selectedItem.transactions[_selectedItem.currentTransaction] - const currentStep = - currentTx.stage === 'complete' - ? _selectedItem.currentTransaction + 1 - : _selectedItem.currentTransaction - - const stepStatus = - currentTx.stage === 'sent' || currentTx.stage === 'failed' - ? 'inProgress' - : 'notStarted' - - return ( - dispatch({ name: 'setFlowStage', payload: 'transaction' })} - {...{ - ..._selectedItem.intro, - onDismiss, - transactions: _selectedItem.transactions, - }} - /> - ) - }, - ) - .otherwise(([_selectedKey, _selectedItem]) => { - if (!_selectedKey || !_selectedItem) return null - const transactionItem = _selectedItem.transactions[_selectedItem.currentTransaction] - const transaction = transactions[transactionItem.name] - - return ( - - ) - })} - - ) -} diff --git a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.test.tsx b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.test.tsx deleted file mode 100644 index e1a30690b..000000000 --- a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.test.tsx +++ /dev/null @@ -1,541 +0,0 @@ -/* eslint-disable no-promise-executor-return */ -import { act, fireEvent, mockFunction, render, screen, userEvent, waitFor } from '@app/test-utils' - -import type { MockedFunctionDeep } from '@vitest/spy' -import { ComponentProps } from 'react' -import { Account, TransactionRequest } from 'viem' -import { estimateGas, prepareTransactionRequest } from 'viem/actions' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { useClient, useConnectorClient, useSendTransaction } from 'wagmi' - -import { useAccountSafely } from '@app/hooks/account/useAccountSafely' -import { useChainName } from '@app/hooks/chain/useChainName' -import { useAddRecentTransaction } from '@app/hooks/transactions/useAddRecentTransaction' -import { useRecentTransactions } from '@app/hooks/transactions/useRecentTransactions' -import { useIsSafeApp } from '@app/hooks/useIsSafeApp' -import { GenericTransaction } from '@app/transaction-flow/types' -import { checkIsSafeApp } from '@app/utils/safe' - -import { useMockedUseQueryOptions } from '../../../../../test/mock/useMockedUseQueryOptions' -import { calculateGasLimit, transactionSuccessHandler } from './query' -import { handleBackToInput, TransactionStageModal } from './TransactionStageModal' -import { makeMockIntersectionObserver } from '../../../../../test/mock/makeMockIntersectionObserver' - -vi.mock('@app/hooks/account/useAccountSafely') -vi.mock('@app/hooks/chain/useChainName') -vi.mock('@app/hooks/useIsSafeApp') -vi.mock('@app/hooks/transactions/useAddRecentTransaction') -vi.mock('@app/hooks/transactions/useRecentTransactions') -vi.mock('@app/hooks/chain/useInvalidateOnBlock') -vi.mock('@app/utils/safe') - -vi.mock('wagmi') -vi.mock('viem/actions') - -const mockTransactionRequest: TransactionRequest = { - data: '0x1896f70a516f53deb2dac3f055f1db1fbd64c12640aa29059477103c3ef28806f15929250000000000000000000000004976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41', - to: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', - from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', - gas: 0x798an, -} -const mockTransaction: GenericTransaction = { - name: 'updateResolver', - data: { - name: 'other-registrant.eth', - contract: 'registry', - resolverAddress: '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41', - oldResolverAddress: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8', - }, -} - -makeMockIntersectionObserver() - -vi.mock('@app/transaction-flow/transaction', () => { - const originalModule = vi.importActual('@app/transaction-flow/transaction') - return { - __esModule: true, - ...originalModule, - createTransactionRequest: () => mockTransactionRequest, - } -}) - -const mockClient = { - request: vi.fn(), -} - -const mockUseClient = mockFunction(useClient) -const mockUseConnectorClient = mockFunction(useConnectorClient) - -const mockEstimateGas = mockFunction(estimateGas) -const mockPrepareTransactionRequest = prepareTransactionRequest as MockedFunctionDeep< - typeof prepareTransactionRequest -> - -const mockUseIsSafeApp = mockFunction(useIsSafeApp) -const mockUseAddRecentTransaction = mockFunction(useAddRecentTransaction) -const mockUseRecentTransactions = mockFunction(useRecentTransactions) -const mockUseAccountSafely = mockFunction(useAccountSafely) -const mockUseChainName = mockFunction(useChainName) -const mockUseSendTransaction = mockFunction(useSendTransaction) -const mockCheckIsSafeApp = checkIsSafeApp as MockedFunctionDeep - -const mockOnDismiss = vi.fn() -const mockDispatch = vi.fn() - -const ComponentWithDefaultProps = ({ - currentStep = 0, - stepCount = 1, - actionName = 'test', - displayItems = [], - transaction = {} as any, -}: Partial>) => ( - -) - -const renderHelper = async (props: Partial> = {}) => { - const renderValue = render() - await waitFor(() => expect(screen.getByTestId('transaction-modal-inner')).toBeVisible(), { - timeout: 350, - }) - return renderValue -} - -const clickRequest = async () => { - await waitFor(() => expect(screen.getByTestId('transaction-modal-confirm-button')).toBeEnabled()) - await userEvent.click(screen.getByTestId('transaction-modal-confirm-button')) -} - -describe('TransactionStageModal', () => { - mockUseRecentTransactions.mockReturnValue([]) - mockUseSendTransaction.mockReturnValue({}) - - beforeEach(() => { - mockUseClient.mockReturnValue({}) - useMockedUseQueryOptions({ - chainId: 1, - address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', - client: mockClient, - }) - mockUseConnectorClient.mockReturnValue({ - data: { account: { address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' } }, - }) - mockUseIsSafeApp.mockReturnValue({ data: false }) - mockEstimateGas.mockReset() - mockPrepareTransactionRequest.mockReset() - // passthrough for the transaction request - mockPrepareTransactionRequest.mockImplementation( - async (_, { parameters: _parameters, account, ...data }) => - ({ ...data, from: (account as Account).address }) as any, - ) - mockUseAccountSafely.mockReturnValue({ address: '0x1234' }) - mockUseChainName.mockReturnValue('ethereum') - mockUseRecentTransactions.mockReturnValue([ - { - status: 'pending', - hash: '0x123', - action: 'test', - key: 'test', - }, - ]) - }) - - it('should render on open', async () => { - await renderHelper() - expect(screen.getByText('transaction.dialog.confirm.title')).toBeVisible() - }) - it('should render display items', async () => { - await renderHelper({ - displayItems: [ - { - label: 'GenericItem', - value: 'GenericValue', - }, - ], - }) - expect(screen.getByText('transaction.itemLabel.GenericItem')).toBeVisible() - expect(screen.getByText('GenericValue')).toBeVisible() - }) - - it('should not render steps if there is only 1 step', async () => { - await renderHelper() - expect(screen.queryByTestId('step-container')).not.toBeInTheDocument() - }) - it('should render steps if there are multiple steps', async () => { - await renderHelper({ stepCount: 2 }) - expect(screen.getByTestId('step-container')).toBeVisible() - }) - describe('stage', () => { - describe('confirm', () => { - it('should show confirm button as disabled if gas is not estimated', async () => { - await renderHelper() - await waitFor(() => - expect(screen.getByTestId('transaction-modal-confirm-button')).toBeDisabled(), - ) - }) - it('should show confirm button as disabled if a unique identifier is undefined', async () => { - mockUseIsSafeApp.mockReturnValue({ data: false }) - mockEstimateGas.mockResolvedValue(1n) - mockUseSendTransaction.mockReturnValue({ - sendTransaction: () => Promise.resolve(), - }) - mockUseAccountSafely.mockReturnValue({ address: undefined }) - await renderHelper({ transaction: mockTransaction }) - await waitFor(() => - expect(screen.getByTestId('transaction-modal-confirm-button')).toBeDisabled(), - ) - }) - it('should disable confirm button and re-estimate gas if a unique identifier is changed', async () => { - mockEstimateGas.mockResolvedValue(1n) - mockUseIsSafeApp.mockReturnValue({ data: false }) - mockUseSendTransaction.mockReturnValue({ - sendTransaction: () => Promise.resolve(), - }) - const { rerender } = await renderHelper({ transaction: mockTransaction }) - await waitFor(() => - expect(screen.getByTestId('transaction-modal-confirm-button')).toBeEnabled(), - ) - expect(mockEstimateGas).toHaveBeenCalledTimes(1) - mockEstimateGas.mockReset() - rerender( - , - ) - expect(screen.getByTestId('transaction-modal-confirm-button')).toBeDisabled() - await waitFor(() => - expect(screen.getByTestId('transaction-modal-confirm-button')).toBeEnabled(), - ) - expect(mockEstimateGas).toHaveBeenCalledTimes(1) - }) - it('should only show confirm button as enabled if gas is estimated and sendTransaction func is defined', async () => { - mockEstimateGas.mockResolvedValue(1n) - mockUseSendTransaction.mockReturnValue({ - sendTransaction: () => Promise.resolve(), - }) - await renderHelper({ transaction: mockTransaction }) - await waitFor(() => - expect(screen.getByTestId('transaction-modal-confirm-button')).toBeEnabled(), - ) - }) - it('should run set sendTransaction on action click', async () => { - mockEstimateGas.mockResolvedValue(1n) - const mockSendTransaction = vi.fn() - mockUseSendTransaction.mockReturnValue({ - sendTransaction: mockSendTransaction, - }) - mockSendTransaction.mockResolvedValue({ - hash: '0x0', - }) - await renderHelper({ transaction: mockTransaction }) - await clickRequest() - expect(mockSendTransaction).toHaveBeenCalled() - }) - it('should show the waiting for wallet button if the transaction is loading', async () => { - mockEstimateGas.mockResolvedValue(1n) - const mockSendTransaction = vi.fn() - mockUseSendTransaction.mockReturnValue({ - sendTransaction: mockSendTransaction, - isPending: true, - }) - mockSendTransaction.mockImplementation(async () => new Promise(() => {})) - await renderHelper({ transaction: mockTransaction }) - await waitFor(() => - expect(screen.getByTestId('transaction-modal-confirm-button')).toBeDisabled(), - ) - }) - it('should show the error message and reenable button if there is an error', async () => { - const mockSendTransaction = vi.fn() - mockUseSendTransaction.mockReturnValue({ - sendTransaction: mockSendTransaction, - error: new Error('error123') as any, - }) - await renderHelper({ transaction: mockTransaction }) - await clickRequest() - await waitFor(() => expect(screen.getByText('error123')).toBeVisible()) - await waitFor(() => - expect(screen.getByTestId('transaction-modal-confirm-button')).toBeEnabled(), - ) - }) - it('should pass the request to send transaction', async () => { - mockUseIsSafeApp.mockReturnValue({ data: false }) - mockEstimateGas.mockResolvedValue(1n) - const mockSendTransaction = vi.fn() - mockUseSendTransaction.mockReturnValue({ - sendTransaction: mockSendTransaction, - }) - await renderHelper({ transaction: mockTransaction }) - await clickRequest() - await waitFor(() => - expect(mockSendTransaction.mock.lastCall![0]!).toStrictEqual( - expect.objectContaining({ - ...mockTransactionRequest, - gas: 1n, - accessList: undefined, - }), - ), - ) - }) - it('should add to recent transactions and run dispatch from success callback', async () => { - const mockAddTransaction = vi.fn() - mockUseAddRecentTransaction.mockReturnValue(mockAddTransaction) - mockCheckIsSafeApp.mockResolvedValue(false) - await renderHelper({ transaction: mockTransaction }) - await waitFor(() => - expect(mockUseSendTransaction.mock.lastCall![0]!.mutation!.onSuccess).toBeDefined(), - ) - await mockUseSendTransaction.mock.lastCall![0]!.mutation!.onSuccess!('0x123', {} as any, {}) - expect(mockAddTransaction).toBeCalledWith( - expect.objectContaining({ - hash: '0x123', - action: 'test', - isSafeTx: false, - key: 'test', - }), - ) - expect(mockDispatch).toBeCalledWith({ - name: 'setTransactionHash', - payload: { hash: '0x123', key: 'test'}, - }) - }) - it('should add to recent transactions and run dispatch from success callback when isSafeTx', async () => { - const mockAddTransaction = vi.fn() - mockUseIsSafeApp.mockReturnValue({ data: 'iframe' }) - mockUseAddRecentTransaction.mockReturnValue(mockAddTransaction) - mockCheckIsSafeApp.mockResolvedValue('iframe') - await renderHelper({ transaction: mockTransaction }) - await waitFor(() => - expect(mockUseSendTransaction.mock.lastCall![0]!.mutation!.onSuccess).toBeDefined(), - ) - await mockUseSendTransaction.mock.lastCall![0]!.mutation!.onSuccess!('0x123', {} as any, {}) - expect(mockAddTransaction).toBeCalledWith( - expect.objectContaining({ - hash: '0x123', - action: 'test', - isSafeTx: true, - key: 'test', - }), - ) - expect(mockDispatch).toBeCalledWith({ - name: 'setTransactionHash', - payload: { hash: '0x123', key: 'test'}, - }) - }) - }) - describe('sent', () => { - it('should show load bar', async () => { - await renderHelper({ - transaction: { ...mockTransaction, hash: '0x123', sendTime: Date.now(), stage: 'sent' }, - }) - await waitFor(() => expect(screen.getByTestId('load-bar-container')).toBeVisible()) - }) - it('should call onDismiss on close', async () => { - await renderHelper({ - transaction: { ...mockTransaction, hash: '0x123', sendTime: Date.now(), stage: 'sent' }, - }) - fireEvent.click(screen.getByTestId('transaction-modal-sent-button')) - expect(mockOnDismiss).toHaveBeenCalled() - }) - it('should show message if transaction is taking a long time', async () => { - await renderHelper({ - transaction: { - ...mockTransaction, - hash: '0x123', - sendTime: Date.now() - 45000, - stage: 'sent', - }, - }) - expect(screen.getByText('transaction.dialog.sent.progress.message')).toBeVisible() - expect(screen.getByText('transaction.dialog.sent.learn')).toBeVisible() - }) - }) - describe('complete', () => { - it('should call onDismiss on close', async () => { - await renderHelper({ - transaction: { - ...mockTransaction, - hash: '0x123', - sendTime: Date.now(), - stage: 'complete', - }, - }) - fireEvent.click(screen.getByTestId('transaction-modal-complete-button')) - expect(mockOnDismiss).toHaveBeenCalled() - }) - }) - describe('failed', () => { - it('should show try again button', async () => { - await renderHelper({ transaction: { ...mockTransaction, hash: '0x123', stage: 'failed' } }) - expect(screen.getByTestId('transaction-modal-failed-button')).toBeVisible() - }) - it('should run sendTransaction on action click', async () => { - mockEstimateGas.mockResolvedValue(1n) - const mockSendTransaction = vi.fn() - mockUseSendTransaction.mockReturnValue({ - sendTransaction: mockSendTransaction, - }) - mockSendTransaction.mockResolvedValue({ - hash: '0x0', - }) - await renderHelper({ transaction: { ...mockTransaction, hash: '0x123', stage: 'failed' } }) - await waitFor(() => - expect(screen.getByTestId('transaction-modal-failed-button')).toBeEnabled(), - ) - await act(async () => { - fireEvent.click(screen.getByTestId('transaction-modal-failed-button')) - }) - expect(mockSendTransaction).toHaveBeenCalled() - }) - }) - }) -}) - -describe('handleBackToInput', () => { - it('should reset the transaction step', () => { - handleBackToInput(mockDispatch)() - expect(mockDispatch).toBeCalledWith({ - name: 'resetTransactionStep', - }) - }) -}) - -describe('transactionSuccessHandler', () => { - it('should add recent transaction data', async () => { - const mockAddRecentTransaction = vi.fn() - - transactionSuccessHandler({ - actionName: 'actionName', - txKey: 'txKey', - request: {} as any, - addRecentTransaction: mockAddRecentTransaction, - dispatch: vi.fn(), - isSafeApp: false, - client: { request: vi.fn(async () => ({ testKey: 'testVal' })) } as any, - connectorClient: { data: { account: { address: '0x1234' } }, request: vi.fn() } as any, - })('0xhash') - - await waitFor(() => - expect(mockAddRecentTransaction).toBeCalledWith( - expect.objectContaining({ testKey: 'testVal' }), - ), - ) - }) - it('should dispatch the correct action', async () => { - const mockDispatch = vi.fn() - - transactionSuccessHandler({ - actionName: 'actionName', - txKey: 'txKey', - request: {} as any, - addRecentTransaction: vi.fn(), - dispatch: mockDispatch, - isSafeApp: false, - client: { request: vi.fn(async () => ({ testKey: 'testVal' })) } as any, - connectorClient: { data: { account: { address: '0x1234' } }, request: vi.fn() } as any, - })('0xhash') - - await waitFor(() => - expect(mockDispatch).toBeCalledWith( - expect.objectContaining({ name: 'setTransactionHash', payload: { hash: '0xhash', key: 'txKey'} }), - ), - ) - }) - it('should handle a failed call to getTransaction', async () => { - const mockAddRecentTransaction = vi.fn() - - transactionSuccessHandler({ - actionName: 'actionName', - txKey: 'txKey', - request: {} as any, - addRecentTransaction: mockAddRecentTransaction, - dispatch: vi.fn(), - isSafeApp: false, - client: { request: vi.fn(async () => Promise.reject(new Error('Error'))) } as any, - connectorClient: { data: { account: { address: '0x1234' } }, request: vi.fn() } as any, - })('0xhash') - - await waitFor(() => expect(mockAddRecentTransaction).toBeCalled()) - }) -}) - -describe('calculateGasLimit', () => { - const mockConnectorClient = { - account: { - address: '0x1234', - }, - } - const mockTxWithZeroGas = { - to: '0x1234567890123456789012345678901234567890', - value: 0n, - data: '0x12345678', - } as const - const mockTransactionName = 'registerName' - const mockIsSafeApp = false - - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should calculate gas limit for non-safe apps', async () => { - mockEstimateGas.mockResolvedValueOnce(100000n) - const result = await calculateGasLimit({ - isSafeApp: mockIsSafeApp, - txWithZeroGas: mockTxWithZeroGas, - transactionName: mockTransactionName, - client: mockClient as any, - connectorClient: mockConnectorClient as any, - }) - expect(result.gasLimit).toEqual(105000n) - expect(result.accessList).toBeUndefined() - expect(mockEstimateGas).toHaveBeenCalledWith(mockClient, { - ...mockTxWithZeroGas, - account: mockConnectorClient.account, - }) - }) - - it('should calculate gas limit for safe apps', async () => { - const mockAccessListResponse = { - gasUsed: '0x64', - accessList: [ - { - address: '0x1234567890123456789012345678901234567890', - storageKeys: ['0x1234567890123456789012345678901234567890123456789012345678901234'], - }, - ], - } - mockClient.request.mockResolvedValueOnce(mockAccessListResponse) - const result = await calculateGasLimit({ - isSafeApp: true, - txWithZeroGas: mockTxWithZeroGas, - transactionName: mockTransactionName, - client: mockClient as any, - connectorClient: mockConnectorClient as any, - }) - expect(result.gasLimit).toEqual(5100n) - expect(result.accessList).toEqual(mockAccessListResponse.accessList) - expect(mockClient.request).toHaveBeenCalledWith({ - method: 'eth_createAccessList', - params: [ - { - from: mockConnectorClient.account.address, - ...mockTxWithZeroGas, - value: '0x0', - }, - 'latest', - ], - }) - }) -}) diff --git a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx deleted file mode 100644 index 27ed8fcd9..000000000 --- a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx +++ /dev/null @@ -1,568 +0,0 @@ -import { queryOptions } from '@tanstack/react-query' -import { Dispatch, useCallback, useEffect, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import styled, { css } from 'styled-components' -import { BaseError } from 'viem' -import { useClient, useConnectorClient, useSendTransaction } from 'wagmi' - -import { - Button, - CrossCircleSVG, - Dialog, - Helper, - QuestionCircleSVG, - Spinner, - Typography, -} from '@ensdomains/thorin' - -import AeroplaneSVG from '@app/assets/Aeroplane.svg' -import CircleTickSVG from '@app/assets/CircleTick.svg' -import WalletSVG from '@app/assets/Wallet.svg' -import { Outlink } from '@app/components/Outlink' -import { useChainName } from '@app/hooks/chain/useChainName' -import { useInvalidateOnBlock } from '@app/hooks/chain/useInvalidateOnBlock' -import { useAddRecentTransaction } from '@app/hooks/transactions/useAddRecentTransaction' -import { useRecentTransactions } from '@app/hooks/transactions/useRecentTransactions' -import { useIsSafeApp } from '@app/hooks/useIsSafeApp' -import { useQueryOptions } from '@app/hooks/useQueryOptions' -import { - ManagedDialogProps, - TransactionFlowAction, - TransactionStage, -} from '@app/transaction-flow/types' -import { ConfigWithEns, TransactionDisplayItem } from '@app/types' -import { getReadableError } from '@app/utils/errors' -import { getIsCachedData } from '@app/utils/getIsCachedData' -import { useQuery } from '@app/utils/query/useQuery' -import { makeEtherscanLink } from '@app/utils/utils' - -import { DisplayItems } from '../DisplayItems' -import { - createTransactionRequestQueryFn, - getTransactionErrorQueryFn, - getUniqueTransaction, - transactionSuccessHandler, -} from './query' - -const BarContainer = styled.div( - ({ theme }) => css` - width: ${theme.space.full}; - display: flex; - flex-direction: column; - align-items: center; - gap: ${theme.space['2']}; - `, -) - -const WalletIcon = styled.svg( - ({ theme }) => css` - width: ${theme.space['12']}; - `, -) - -const Bar = styled.div<{ $status: Status }>( - ({ theme, $status }) => css` - width: ${theme.space.full}; - height: ${theme.space['9']}; - border-radius: ${theme.radii.full}; - background-color: ${theme.colors.blueSurface}; - overflow: hidden; - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - - --bar-color: ${theme.colors.blue}; - - ${$status === 'complete' && - css` - --bar-color: ${theme.colors.green}; - `} - ${$status === 'failed' && - css` - --bar-color: ${theme.colors.red}; - `} - `, -) - -const BarTypography = styled(Typography)( - ({ theme }) => css` - color: ${theme.colors.background}; - font-weight: ${theme.fontWeights.bold}; - `, -) - -const ProgressTypography = styled(Typography)( - ({ theme }) => css` - color: ${theme.colors.accent}; - font-weight: ${theme.fontWeights.bold}; - text-align: center; - `, -) - -const AeroplaneIcon = styled.svg( - ({ theme }) => css` - width: ${theme.space['4']}; - height: ${theme.space['4']}; - color: ${theme.colors.background}; - `, -) - -const CircleIcon = styled.svg( - ({ theme }) => css` - width: ${theme.space['6']}; - height: ${theme.space['6']}; - color: ${theme.colors.background}; - `, -) - -const MessageTypography = styled(Typography)( - () => css` - text-align: center; - `, -) - -type Status = Omit - -const BarPrefix = styled.div( - ({ theme }) => css` - padding: ${theme.space['2']} ${theme.space['4']}; - width: min-content; - white-space: nowrap; - height: ${theme.space['9']}; - margin-right: -1px; - - border-radius: ${theme.radii.full}; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - background-color: var(--bar-color); - `, -) - -const InnerBar = styled.div( - ({ theme }) => css` - padding: ${theme.space['2']} ${theme.space['4']}; - height: ${theme.space['9']}; - - border-radius: ${theme.radii.full}; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - - transition: width 1s linear; - &.progress-complete { - width: 100% !important; - padding-right: ${theme.space['2']}; - transition: width 0.5s ease-in-out; - } - - background-color: var(--bar-color); - - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; - - position: relative; - - & > svg { - position: absolute; - right: ${theme.space['2']}; - top: 50%; - transform: translateY(-50%); - } - `, -) - -export const LoadBar = ({ status, sendTime }: { status: Status; sendTime: number | undefined }) => { - const { t } = useTranslation() - - const time = useMemo(() => ({ start: sendTime || Date.now(), ms: 45000 }), [sendTime]) - const [{ progress }, setProgress] = useState({ progress: 0, timeLeft: 45 }) - - const intervalFunc = useCallback( - (interval?: NodeJS.Timeout) => { - const timeElapsed = Date.now() - time.start - const _timeLeft = time.ms - timeElapsed - const _progress = Math.min((timeElapsed / (timeElapsed + _timeLeft)) * 100, 100) - setProgress({ timeLeft: Math.floor(_timeLeft / 1000), progress: _progress }) - if (_progress === 100) clearInterval(interval) - }, - [time.ms, time.start], - ) - - useEffect(() => { - intervalFunc() - const interval = setInterval(intervalFunc, 1000) - return () => clearInterval(interval) - }, [intervalFunc]) - - const message = useMemo(() => { - if (status === 'complete') { - return t('transaction.dialog.complete.message') - } - if (status === 'failed') { - return null - } - return t('transaction.dialog.sent.message') - }, [status, t]) - - const isTakingLongerThanExpected = status === 'sent' && progress === 100 - - const progressMessage = useMemo(() => { - if (isTakingLongerThanExpected) { - return ( - - {t('transaction.dialog.sent.learn')} - - ) - } - return null - }, [isTakingLongerThanExpected, t]) - - const EndElement = useMemo(() => { - if (status === 'complete') { - return - } - if (status === 'failed') { - return - } - if (progress !== 100) { - return - } - return - }, [progress, status]) - - return ( - <> - - - - - {t( - isTakingLongerThanExpected - ? 'transaction.dialog.sent.progress.message' - : `transaction.dialog.${status}.progress.title`, - )} - - - - {EndElement} - - - {progressMessage && {progressMessage}} - - {message && {message}} - - ) -} - -export const handleBackToInput = (dispatch: Dispatch) => () => { - dispatch({ name: 'setFlowStage', payload: 'input' }) - dispatch({ name: 'resetTransactionStep' }) -} - -function useCreateSubnameRedirect( - shouldTrigger: boolean, - subdomain?: TransactionDisplayItem['value'], -) { - useEffect(() => { - if (shouldTrigger && typeof subdomain === 'string') { - setTimeout(() => { - window.location.href = `/${subdomain}` - }, 1000) - } - }, [shouldTrigger, subdomain]) -} - -export const TransactionStageModal = ({ - actionName, - currentStep, - displayItems, - helper, - dispatch, - stepCount, - transaction, - txKey, - onDismiss, - backToInput, -}: ManagedDialogProps) => { - const { t } = useTranslation() - const chainName = useChainName() - - const { data: isSafeApp, isLoading: safeAppStatusLoading } = useIsSafeApp() - const { data: connectorClient } = useConnectorClient() - const client = useClient() - - const addRecentTransaction = useAddRecentTransaction() - - const stage = transaction.stage || 'confirm' - const recentTransactions = useRecentTransactions() - const transactionStatus = useMemo( - () => recentTransactions.find((tx) => tx.hash === transaction.hash)?.status, - [recentTransactions, transaction.hash], - ) - - const uniqueTxIdentifiers = useMemo( - () => - getUniqueTransaction({ - txKey, - currentStep, - transaction, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [txKey, currentStep, transaction?.name, transaction?.data], - ) - - // if not all unique identifiers are defined, there could be incorrect cached data - const isUniquenessDefined = useMemo( - // number check is for if step = 0 - () => Object.values(uniqueTxIdentifiers).every((val) => typeof val === 'number' || !!val), - [uniqueTxIdentifiers], - ) - - const canEnableTransactionRequest = useMemo( - () => - !!transaction && - !!connectorClient?.account && - !safeAppStatusLoading && - !(stage === 'sent' || stage === 'complete') && - isUniquenessDefined, - [transaction, connectorClient?.account, safeAppStatusLoading, stage, isUniquenessDefined], - ) - - const initialOptions = useQueryOptions({ - params: uniqueTxIdentifiers, - functionName: 'createTransactionRequest', - queryDependencyType: 'standard', - queryFn: createTransactionRequestQueryFn, - }) - - const preparedOptions = queryOptions({ - queryKey: initialOptions.queryKey, - queryFn: initialOptions.queryFn({ connectorClient, isSafeApp }), - }) - - const transactionRequestQuery = useQuery({ - ...preparedOptions, - enabled: canEnableTransactionRequest, - refetchOnMount: 'always', - }) - - const { data: request, isLoading: requestLoading, error: requestError } = transactionRequestQuery - const isTransactionRequestCachedData = getIsCachedData(transactionRequestQuery) - - useInvalidateOnBlock({ - enabled: canEnableTransactionRequest && process.env.NEXT_PUBLIC_ETH_NODE !== 'anvil', - queryKey: preparedOptions.queryKey, - }) - - const { - isPending: transactionLoading, - error: transactionError, - sendTransaction, - } = useSendTransaction({ - mutation: { - onSuccess: transactionSuccessHandler({ - client, - connectorClient: connectorClient!, - actionName, - txKey, - request, - addRecentTransaction, - dispatch, - isSafeApp, - }), - }, - }) - - useCreateSubnameRedirect( - stage === 'complete' && currentStep + 1 === stepCount, - displayItems.find((i) => i.label === 'subname' && i.type === 'name')?.value, - ) - - const FilledDisplayItems = useMemo( - () => , - [displayItems], - ) - - const MiddleContent = useMemo(() => { - if (stage !== 'confirm') { - return - } - return ( - <> - - {t('transaction.dialog.confirm.message')} - - ) - }, [stage, t, transaction.sendTime]) - - const HelperContent = useMemo(() => { - if (!helper) return null - return - }, [helper]) - - const ActionButton = useMemo(() => { - if (stage === 'complete') { - const final = currentStep + 1 === stepCount - - if (final) { - return ( - - ) - } - return ( - - ) - } - if (stage === 'failed') { - return ( - - ) - } - if (stage === 'sent') { - return ( - - ) - } - if (transactionLoading) { - return ( - - ) - } - return ( - - ) - }, [ - canEnableTransactionRequest, - currentStep, - dispatch, - onDismiss, - requestError, - requestLoading, - sendTransaction, - stage, - stepCount, - t, - transactionLoading, - request, - isTransactionRequestCachedData, - ]) - - const stepStatus = useMemo(() => { - if (stage === 'complete') { - return 'completed' - } - return 'inProgress' - }, [stage]) - - const initialErrorOptions = useQueryOptions({ - params: { hash: transaction.hash, status: transactionStatus }, - functionName: 'getTransactionError', - queryDependencyType: 'standard', - queryFn: getTransactionErrorQueryFn, - }) - - const preparedErrorOptions = queryOptions({ - queryKey: initialErrorOptions.queryKey, - queryFn: initialErrorOptions.queryFn, - }) - - const { data: upperError } = useQuery({ - ...preparedErrorOptions, - enabled: !!transaction && !!transaction.hash && transactionStatus === 'failed', - }) - - const lowerError = useMemo(() => { - if (stage === 'complete' || stage === 'sent') return null - const err = transactionError || requestError - if (!err) return null - if (!(err instanceof BaseError)) { - if ('message' in err) return err.message - return t('transaction.error.unknown') - } - const readableError = getReadableError(err) - return readableError || err.shortMessage - }, [t, stage, transactionError, requestError]) - - return ( - <> - - - {MiddleContent} - {upperError && {t(upperError)}} - {FilledDisplayItems} - {HelperContent} - {transaction.hash && ( - - {t('transaction.viewEtherscan')} - - )} - {lowerError && {lowerError}} - - 1 ? stepCount : undefined} - stepStatus={stepStatus} - trailing={ActionButton} - leading={ - backToInput && - !(stage === 'sent' || stage === 'complete') && ( - - ) - } - /> - - ) -} diff --git a/src/components/@molecules/VerificationBadge/VerificationBadge.tsx b/src/components/@molecules/VerificationBadge/VerificationBadge.tsx index eb4a17eca..9f136d4ea 100644 --- a/src/components/@molecules/VerificationBadge/VerificationBadge.tsx +++ b/src/components/@molecules/VerificationBadge/VerificationBadge.tsx @@ -6,7 +6,7 @@ import { AlertSVG, Colors, Tooltip } from '@ensdomains/thorin' import VerifiedPersonSVG from '@app/assets/VerifiedPerson.svg' import VerifiedRecordSVG from '@app/assets/VerifiedRecord.svg' -import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' type Color = Extract diff --git a/src/components/@molecules/VerificationBadge/components/VerificationBadgeAccountTooltipContent.tsx b/src/components/@molecules/VerificationBadge/components/VerificationBadgeAccountTooltipContent.tsx index af599e5b0..fc274f72e 100644 --- a/src/components/@molecules/VerificationBadge/components/VerificationBadgeAccountTooltipContent.tsx +++ b/src/components/@molecules/VerificationBadge/components/VerificationBadgeAccountTooltipContent.tsx @@ -4,7 +4,7 @@ import { match } from 'ts-pattern' import { Colors, OutlinkSVG, Typography } from '@ensdomains/thorin' import DentitySVG from '@app/assets/verification/Dentity.svg' -import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' type Props = { verifiers?: VerificationProtocol[] } diff --git a/src/components/@molecules/VerificationBadge/components/VerificationBadgeVerifierTooltipContent.tsx b/src/components/@molecules/VerificationBadge/components/VerificationBadgeVerifierTooltipContent.tsx index e136862ce..077717bdc 100644 --- a/src/components/@molecules/VerificationBadge/components/VerificationBadgeVerifierTooltipContent.tsx +++ b/src/components/@molecules/VerificationBadge/components/VerificationBadgeVerifierTooltipContent.tsx @@ -5,7 +5,7 @@ import { Colors, Typography } from '@ensdomains/thorin' import VerifiedPersonSVG from '@app/assets/VerifiedPerson.svg' import { SupportOutlink } from '@app/components/@atoms/SupportOutlink' -import { CenteredTypography } from '@app/transaction-flow/input/ProfileEditor/components/CenteredTypography' +import { CenteredTypography } from '@app/transaction/user/input/ProfileEditor/components/CenteredTypography' const Container = styled.div<{ $color: Colors }>( ({ theme, $color }) => css` diff --git a/src/components/Notifications.tsx b/src/components/Notifications.tsx deleted file mode 100644 index 811050a68..000000000 --- a/src/components/Notifications.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import styled, { css } from 'styled-components' - -import { Button, Toast } from '@ensdomains/thorin' - -import { useChainName } from '@app/hooks/chain/useChainName' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' -import { useBreakpoint } from '@app/utils/BreakpointProvider' -import { UpdateCallback, useCallbackOnTransaction } from '@app/utils/SyncProvider/SyncProvider' -import { makeEtherscanLink } from '@app/utils/utils' - -import { trackEvent } from '../utils/analytics' - -type Notification = { - title: string - description?: string - children?: React.ReactNode -} - -const ButtonContainer = styled.div( - ({ theme }) => css` - display: flex; - flex-direction: row; - align-items: center; - justify-content: stretch; - gap: ${theme.space['2']}; - `, -) - -export const Notifications = () => { - const { t } = useTranslation() - const breakpoints = useBreakpoint() - - const chainName = useChainName() - - const [open, setOpen] = useState(false) - - const { resumeTransactionFlow, getResumable } = useTransactionFlow() - - const [notificationQueue, setNotificationQueue] = useState([]) - const currentNotification = notificationQueue[0] - - const updateCallback = useCallback( - ({ action, key, status, hash }) => { - if (status === 'pending' || status === 'repriced') return - if (status === 'confirmed') { - switch (action) { - case 'registerName': - trackEvent('register', chainName) - break - case 'commitName': - trackEvent('commit', chainName) - break - case 'extendNames': - trackEvent('renew', chainName) - break - default: - break - } - } - const resumable = key && getResumable(key) - const item = { - title: t(`transaction.status.${status}.notifyTitle`), - description: t(`transaction.status.${status}.notifyMessage`, { - action: t(`transaction.description.${action}`), - }), - children: resumable ? ( - - - - - - - ) : ( - - - - ), - } - - setNotificationQueue((queue) => [...queue, item]) - }, - [chainName, getResumable, resumeTransactionFlow, t], - ) - - useCallbackOnTransaction(updateCallback) - - useEffect(() => { - if (currentNotification) { - setOpen(true) - } - }, [currentNotification]) - - return ( - { - setOpen(false) - setTimeout( - () => setNotificationQueue((prev) => [...prev.filter((x) => x !== currentNotification)]), - 300, - ) - }} - open={open} - variant={breakpoints.sm ? 'desktop' : 'touch'} - {...currentNotification} - /> - ) -} diff --git a/src/components/Notifications2.tsx b/src/components/Notifications2.tsx new file mode 100644 index 000000000..3b145b6a4 --- /dev/null +++ b/src/components/Notifications2.tsx @@ -0,0 +1,120 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' + +import { Button, Toast } from '@ensdomains/thorin' + +import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' +import type { StoredTransaction } from '@app/transaction/slices/createTransactionSlice' +import { useTransactionManager } from '@app/transaction/transactionManager' +import { useBreakpoint } from '@app/utils/BreakpointProvider' +import { createEtherscanLink } from '@app/utils/utils' + +const ButtonContainer = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: row; + align-items: center; + justify-content: stretch; + gap: ${theme.space['2']}; + `, +) + +type SuccessOrRevertedTransaction = Extract + +const Notification = ({ + transaction, + onClose, + open, +}: { + transaction: SuccessOrRevertedTransaction | null + onClose: () => void + open: boolean +}) => { + const { t } = useTranslation() + const router = useRouterWithHistory() + const breakpoints = useBreakpoint() + + const isFlowResumable = useTransactionManager((s) => s.isFlowResumable) + const resumeFlow = useTransactionManager((s) => s.resumeFlowWithCheck) + + const resumable = transaction && isFlowResumable(transaction.flowId) + + const button = (() => { + if (!transaction) return null + + const etherscanLink = createEtherscanLink({ + data: transaction.currentHash, + chainId: transaction.targetChainId, + }) + + if (!resumable) + return ( + + + + ) + + return ( + + + + + + + ) + })() + + const toastProps = transaction + ? { + title: t(`transaction.status.${transaction.status}.notifyTitle`), + description: t(`transaction.status.${transaction.status}.notifyMessage`, { + action: t(`transaction.description.${transaction.name}`), + }), + children: button, + } + : { + title: '', + } + + return ( + + ) +} + +export const Notifications = () => { + const [shouldHide, setShouldHide] = useState(false) + const currentNotification = useTransactionManager((s) => s.currentNotification) + const dismissNotification = useTransactionManager((s) => s.dismissNotification) + + const open = currentNotification !== null && !shouldHide + + return ( + { + setShouldHide(true) + setTimeout(() => { + dismissNotification() + setShouldHide(false) + }, 300) + }} + open={open} + transaction={currentNotification} + /> + ) +} diff --git a/src/components/ProfileSnippet.tsx b/src/components/ProfileSnippet.tsx index a478fea35..41edca7b5 100644 --- a/src/components/ProfileSnippet.tsx +++ b/src/components/ProfileSnippet.tsx @@ -9,8 +9,8 @@ import VerifiedPersonSVG from '@app/assets/VerifiedPerson.svg' import { useAbilities } from '@app/hooks/abilities/useAbilities' import { useBeautifiedName } from '@app/hooks/useBeautifiedName' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' -import { useTransactionFlow } from '../transaction-flow/TransactionFlowProvider' import { NameAvatar } from './AvatarWithZorb' const Container = styled.div<{ $banner?: string }>( @@ -189,7 +189,6 @@ export const ProfileSnippet = ({ const router = useRouterWithHistory() const { t } = useTranslation('common') - const { usePreparedDataInput } = useTransactionFlow() const showExtendNamesInput = usePreparedDataInput('ExtendNames') const abilities = useAbilities({ name }) diff --git a/src/components/pages/VerificationErrorDialog.tsx b/src/components/pages/VerificationErrorDialog.tsx index 77ec80c22..1f52cf48b 100644 --- a/src/components/pages/VerificationErrorDialog.tsx +++ b/src/components/pages/VerificationErrorDialog.tsx @@ -2,7 +2,7 @@ import { ComponentProps } from 'react' import { Button, Dialog } from '@ensdomains/thorin' -import { CenteredTypography } from '@app/transaction-flow/input/ProfileEditor/components/CenteredTypography' +import { CenteredTypography } from '@app/transaction/user/input/ProfileEditor/components/CenteredTypography' export type ButtonProps = ComponentProps diff --git a/src/components/pages/import/[name]/DnsClaim.tsx b/src/components/pages/import/[name]/DnsClaim.tsx index f114d8e0d..b480e736a 100644 --- a/src/components/pages/import/[name]/DnsClaim.tsx +++ b/src/components/pages/import/[name]/DnsClaim.tsx @@ -7,7 +7,7 @@ import { useAccount } from 'wagmi' import { useBasicName } from '@app/hooks/useBasicName' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { Content } from '@app/layouts/Content' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { useTransactionManager } from '@app/transaction/transactionManager' import { shouldRedirect } from '@app/utils/shouldRedirect' import { CompleteImport } from './steps/CompleteImport' @@ -52,7 +52,7 @@ export const DnsClaim = () => { const key = `importDnsName-${selected.name}` - const { cleanupFlow } = useTransactionFlow() + const cleanupFlow = useTransactionManager((s) => s.cleanupFlow) useEffect(() => { const handleRouteChange = (e: string) => { diff --git a/src/components/pages/import/[name]/steps/SelectImportType.tsx b/src/components/pages/import/[name]/steps/SelectImportType.tsx index 97ddbee25..df0f20971 100644 --- a/src/components/pages/import/[name]/steps/SelectImportType.tsx +++ b/src/components/pages/import/[name]/steps/SelectImportType.tsx @@ -12,7 +12,7 @@ import { useDnsOffchainStatus } from '@app/hooks/dns/useDnsOffchainStatus' import { useDnsSecEnabled } from '@app/hooks/dns/useDnsSecEnabled' import { useDnsOwner } from '@app/hooks/ensjs/dns/useDnsOwner' import { useResolver } from '@app/hooks/ensjs/public/useResolver' -import { CenteredTypography } from '@app/transaction-flow/input/ProfileEditor/components/CenteredTypography' +import { CenteredTypography } from '@app/transaction/user/ProfileEditor/components/CenteredTypography' import { getSupportLink } from '@app/utils/supportLinks' import { DnsImportActionButton, DnsImportCard, DnsImportHeading } from '../shared' diff --git a/src/components/pages/import/[name]/utils.ts b/src/components/pages/import/[name]/utils.ts index a36320a2d..e2a3e0ca6 100644 --- a/src/components/pages/import/[name]/utils.ts +++ b/src/components/pages/import/[name]/utils.ts @@ -12,7 +12,7 @@ import type { GetDnsImportDataReturnType } from '@ensdomains/ensjs/dns' import { addStateOverride } from '@app/hooks/chain/useEstimateGasWithStateOverride' import type { UseDnsOwnerError } from '@app/hooks/ensjs/dns/useDnsOwner' -import { createTransactionItem } from '@app/transaction-flow/transaction' +import { createUserTransaction } from '@app/transaction/user/transaction' export type DnsNavigationFunction = (direction: 'prev' | 'next') => void @@ -68,17 +68,17 @@ export const createImportTransactionRequests = ({ dnsRegistrarAddress: Address }) => { const createApproveTx = () => - createTransactionItem('approveDnsRegistrar', { + createUserTransaction('approveDnsRegistrar', { address, }) const createClaimTx = () => - createTransactionItem('claimDnsName', { + createUserTransaction('claimDnsName', { name, dnsImportData, address, }) const createImportTx = () => - createTransactionItem('importDnsName', { + createUserTransaction('importDnsName', { name, dnsImportData, }) diff --git a/src/components/pages/profile/ProfileButton.tsx b/src/components/pages/profile/ProfileButton.tsx index 5cac0e0b0..d60941d5f 100644 --- a/src/components/pages/profile/ProfileButton.tsx +++ b/src/components/pages/profile/ProfileButton.tsx @@ -26,11 +26,11 @@ import { VerificationBadge } from '@app/components/@molecules/VerificationBadge/ import { useCoinChain } from '@app/hooks/chain/useCoinChain' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import { getDestination } from '@app/routes' -import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' import { useBreakpoint } from '@app/utils/BreakpointProvider' import { getContentHashLink } from '@app/utils/contenthash' import { getSocialData } from '@app/utils/getSocialData' -import { makeEtherscanLink, shortenAddress } from '@app/utils/utils' +import { createEtherscanLink, shortenAddress } from '@app/utils/utils' import { getVerifierData } from '@app/utils/verification/getVerifierData' import { isVerificationProtocol } from '@app/utils/verification/isVerificationProtocol' @@ -247,6 +247,7 @@ export const OwnerProfileButton = ({ }) => { const { t } = useTranslation('common') const breakpoints = useBreakpoint() + const chainId = useChainId() const dataType = useMemo(() => { if (!addressOrNameOrDate) @@ -351,7 +352,7 @@ export const OwnerProfileButton = ({ { icon: , label: 'View on Etherscan', - href: makeEtherscanLink(addressOrNameOrDate, 'mainnet', 'address'), + href: createEtherscanLink({ data: addressOrNameOrDate, chainId, route: 'address' }), }, ] as DropdownItem[]) : []), diff --git a/src/components/pages/profile/[name]/Profile.tsx b/src/components/pages/profile/[name]/Profile.tsx index 68847c87f..198e74db1 100644 --- a/src/components/pages/profile/[name]/Profile.tsx +++ b/src/components/pages/profile/[name]/Profile.tsx @@ -3,14 +3,13 @@ import { useEffect, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { match } from 'ts-pattern' -import { useAccount } from 'wagmi' +import { useAccount, useChainId } from 'wagmi' import { Banner, CheckCircleSVG, Typography } from '@ensdomains/thorin' import BaseLink from '@app/components/@atoms/BaseLink' import { Outlink } from '@app/components/Outlink' import { useAbilities } from '@app/hooks/abilities/useAbilities' -import { useChainName } from '@app/hooks/chain/useChainName' import { useNameDetails } from '@app/hooks/useNameDetails' import { useProtectedRoute } from '@app/hooks/useProtectedRoute' import { useQueryParameterState } from '@app/hooks/useQueryParameterState' @@ -18,7 +17,7 @@ import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { Content, ContentWarning } from '@app/layouts/Content' import { OG_IMAGE_URL } from '@app/utils/constants' import { shouldRedirect } from '@app/utils/shouldRedirect' -import { formatFullExpiry, makeEtherscanLink } from '@app/utils/utils' +import { createEtherscanLink, formatFullExpiry } from '@app/utils/utils' import { ProfileEmptyBanner } from './ProfileEmptyBanner' import MoreTab from './tabs/MoreTab/MoreTab' @@ -224,7 +223,7 @@ const ProfileContent = ({ isSelf, isLoading: parentIsLoading, name }: Props) => const ogImageUrl = `${OG_IMAGE_URL}/name/${normalisedName || name}` - const chainName = useChainName() + const chainId = useChainId() return ( <> @@ -269,7 +268,7 @@ const ProfileContent = ({ isSelf, isLoading: parentIsLoading, name }: Props) => titleExtra: profile?.address ? ( {t('etherscan', { ns: 'common' })} diff --git a/src/components/pages/profile/[name]/ProfileEmptyBanner.tsx b/src/components/pages/profile/[name]/ProfileEmptyBanner.tsx index afe7ed287..0b69f4a67 100644 --- a/src/components/pages/profile/[name]/ProfileEmptyBanner.tsx +++ b/src/components/pages/profile/[name]/ProfileEmptyBanner.tsx @@ -7,7 +7,7 @@ import StarsSVG from '@app/assets/Stars.svg' import { useProfileActions } from '@app/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions' import { useProfile } from '@app/hooks/useProfile' -import { profileToProfileRecords } from './registration/steps/Profile/profileRecordUtils' +import { profileToProfileRecords } from '../../register/steps/Profile/profileRecordUtils' const Container = styled.div( ({ theme }) => css` diff --git a/src/components/pages/profile/[name]/registration/steps/Pricing/Pricing.tsx b/src/components/pages/profile/[name]/registration/steps/Pricing/Pricing.tsx deleted file mode 100644 index dc0413a6c..000000000 --- a/src/components/pages/profile/[name]/registration/steps/Pricing/Pricing.tsx +++ /dev/null @@ -1,613 +0,0 @@ -import { Dispatch, SetStateAction, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { usePreviousDistinct } from 'react-use' -import usePrevious from 'react-use/lib/usePrevious' -import styled, { css } from 'styled-components' -import { match, P } from 'ts-pattern' -import type { Address } from 'viem' -import { useBalance } from 'wagmi' -import { GetBalanceData } from 'wagmi/query' - -import { - Button, - Field, - Heading, - Helper, - mq, - RadioButton, - RadioButtonGroup, - Toggle, - Typography, -} from '@ensdomains/thorin' - -import MoonpayLogo from '@app/assets/MoonpayLogo.svg' -import MobileFullWidth from '@app/components/@atoms/MobileFullWidth' -import { RegistrationTimeComparisonBanner } from '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner' -import { Spacer } from '@app/components/@atoms/Spacer' -import { DateSelection } from '@app/components/@molecules/DateSelection/DateSelection' -import { Card } from '@app/components/Card' -import { ConnectButton } from '@app/components/ConnectButton' -import { useAccountSafely } from '@app/hooks/account/useAccountSafely' -import { useContractAddress } from '@app/hooks/chain/useContractAddress' -import { useEstimateFullRegistration } from '@app/hooks/gasEstimation/useEstimateRegistration' -import { useBreakpoint } from '@app/utils/BreakpointProvider' -import { ONE_DAY, ONE_YEAR } from '@app/utils/time' - -import FullInvoice from '../../FullInvoice' -import { - MoonpayTransactionStatus, - PaymentMethod, - RegistrationReducerDataItem, - RegistrationStepData, -} from '../../types' -import { useMoonpayRegistration } from '../../useMoonpayRegistration' -import TemporaryPremium from './TemporaryPremium' - -const StyledCard = styled(Card)( - ({ theme }) => css` - max-width: 780px; - margin: 0 auto; - flex-direction: column; - gap: ${theme.space['4']}; - padding: ${theme.space['4']}; - - ${mq.sm.min(css` - padding: ${theme.space['6']} ${theme.space['18']}; - gap: ${theme.space['6']}; - `)} - `, -) - -const OutlinedContainer = styled.div( - ({ theme }) => css` - width: ${theme.space.full}; - display: grid; - align-items: center; - grid-template-areas: 'title checkbox' 'description description'; - gap: ${theme.space['2']}; - - padding: ${theme.space['4']}; - border-radius: ${theme.radii.large}; - background: ${theme.colors.backgroundSecondary}; - - ${mq.sm.min(css` - grid-template-areas: 'title checkbox' 'description checkbox'; - `)} - `, -) - -const StyledHeading = styled(Heading)( - () => css` - width: 100%; - word-break: break-all; - - @supports (overflow-wrap: anywhere) { - overflow-wrap: anywhere; - word-break: normal; - } - `, -) - -const gridAreaStyle = ({ $name }: { $name: string }) => css` - grid-area: ${$name}; -` - -const moonpayInfoItems = Array.from({ length: 2 }, (_, i) => `steps.info.moonpayItems.${i}`) - -const PaymentChoiceContainer = styled.div` - width: 100%; -` - -const StyledRadioButtonGroup = styled(RadioButtonGroup)( - ({ theme }) => css` - border: 1px solid ${theme.colors.border}; - border-radius: ${theme.radii.large}; - gap: 0; - `, -) - -const StyledRadioButton = styled(RadioButton)`` - -const RadioButtonContainer = styled.div( - ({ theme }) => css` - padding: ${theme.space['4']}; - &:last-child { - border-top: 1px solid ${theme.colors.border}; - } - `, -) - -const StyledTitle = styled(Typography)` - margin-left: 15px; -` - -const RadioLabel = styled(Typography)( - ({ theme }) => css` - margin-right: 10px; - color: ${theme.colors.text}; - `, -) - -const MoonpayContainer = styled.div` - display: flex; - align-items: center; - justify-content: center; - gap: 5px; -` - -const InfoItems = styled.div( - ({ theme }) => css` - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - gap: ${theme.space['4']}; - - ${mq.sm.min(css` - flex-direction: row; - align-items: stretch; - `)} - `, -) - -const InfoItem = styled.div( - ({ theme }) => css` - width: 100%; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: ${theme.space['4']}; - - padding: ${theme.space['4']}; - border: 1px solid ${theme.colors.border}; - border-radius: ${theme.radii.large}; - text-align: center; - - & > div:first-of-type { - width: ${theme.space['10']}; - height: ${theme.space['10']}; - display: flex; - align-items: center; - justify-content: center; - font-size: ${theme.fontSizes.extraLarge}; - font-weight: ${theme.fontWeights.bold}; - color: ${theme.colors.backgroundPrimary}; - background: ${theme.colors.accentPrimary}; - border-radius: ${theme.radii.full}; - } - - & > div:last-of-type { - flex-grow: 1; - display: flex; - align-items: center; - justify-content: center; - } - `, -) - -const LabelContainer = styled.div` - display: flex; - flex-wrap: wrap; -` - -const CheckboxWrapper = styled.div( - () => css` - width: 100%; - `, - gridAreaStyle, -) - -const OutlinedContainerDescription = styled(Typography)(gridAreaStyle) - -const OutlinedContainerTitle = styled(Typography)( - ({ theme }) => css` - font-size: ${theme.fontSizes.large}; - font-weight: ${theme.fontWeights.bold}; - white-space: nowrap; - `, - gridAreaStyle, -) - -const EthInnerCheckbox = ({ - address, - hasPrimaryName, - reverseRecord, - setReverseRecord, - started, -}: { - address: string - hasPrimaryName: boolean - reverseRecord: boolean - setReverseRecord: (val: boolean) => void - started: boolean -}) => { - const { t } = useTranslation('register') - const breakpoints = useBreakpoint() - - useEffect(() => { - if (!started) { - setReverseRecord(!hasPrimaryName) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setReverseRecord]) - - return ( - - - {(ids) => ( - { - e.stopPropagation() - setReverseRecord(e.target.checked) - }} - data-testid="primary-name-toggle" - /> - )} - - - ) -} - -const PaymentChoice = ({ - paymentMethodChoice, - setPaymentMethodChoice, - hasEnoughEth, - hasPendingMoonpayTransaction, - hasFailedMoonpayTransaction, - address, - hasPrimaryName, - reverseRecord, - setReverseRecord, - started, -}: { - paymentMethodChoice: PaymentMethod - setPaymentMethodChoice: Dispatch> - hasEnoughEth: boolean - hasPendingMoonpayTransaction: boolean - hasFailedMoonpayTransaction: boolean - address: string - hasPrimaryName: boolean - reverseRecord: boolean - setReverseRecord: (reverseRecord: boolean) => void - started: boolean -}) => { - const { t } = useTranslation('register') - - return ( - - - {t('steps.info.paymentMethod')} - - - setPaymentMethodChoice(e.target.value as PaymentMethod)} - > - - {t('steps.info.ethereum')}} - name="RadioButtonGroup" - value={PaymentMethod.ethereum} - disabled={hasPendingMoonpayTransaction} - checked={paymentMethodChoice === PaymentMethod.ethereum || undefined} - /> - {paymentMethodChoice === PaymentMethod.ethereum && !hasEnoughEth && ( - <> - - - {t('steps.info.notEnoughEth')} - - - - )} - {paymentMethodChoice === PaymentMethod.ethereum && hasEnoughEth && ( - <> - - - - {t('steps.pricing.primaryName')} - - - - {t('steps.pricing.primaryNameMessage')} - - - - - )} - - - - {t('steps.info.creditOrDebit')} - - ({t('steps.info.additionalFee')}) - - - } - name="RadioButtonGroup" - value={PaymentMethod.moonpay} - checked={paymentMethodChoice === PaymentMethod.moonpay || undefined} - /> - {paymentMethodChoice === PaymentMethod.moonpay && ( - <> - - - {moonpayInfoItems.map((item, idx) => ( - - {idx + 1} - {t(item)} - - ))} - - - {hasFailedMoonpayTransaction && ( - {t('steps.info.failedMoonpayTransaction')} - )} - - - {t('steps.info.poweredBy')} - - - - )} - - - - ) -} - -export type ActionButtonProps = { - address?: Address - hasPendingMoonpayTransaction: boolean - hasFailedMoonpayTransaction: boolean - paymentMethodChoice: PaymentMethod | '' - reverseRecord: boolean - callback: (props: RegistrationStepData['pricing']) => void - initiateMoonpayRegistrationMutation: ReturnType< - typeof useMoonpayRegistration - >['initiateMoonpayRegistrationMutation'] - seconds: number - balance: GetBalanceData | undefined - totalRequiredBalance?: bigint - durationType: 'date' | 'years' -} - -export const ActionButton = (props: ActionButtonProps) => { - const { t } = useTranslation('register') - - return match(props) - .with({ address: P.nullish }, () => ) - .with({ hasPendingMoonpayTransaction: true }, () => ( - - )) - .with({ hasFailedMoonpayTransaction: true, paymentMethodChoice: PaymentMethod.moonpay }, () => ( - - )) - .with( - { paymentMethodChoice: PaymentMethod.moonpay }, - ({ - initiateMoonpayRegistrationMutation, - reverseRecord, - seconds, - paymentMethodChoice, - durationType, - callback, - }) => ( - - ), - ) - .with( - P.when((_props) => typeof _props.balance?.value !== 'bigint' || !_props.totalRequiredBalance), - () => ( - - ), - ) - .with( - P.when( - (_props) => - _props.totalRequiredBalance && - typeof _props.balance?.value === 'bigint' && - _props.balance.value < _props.totalRequiredBalance && - _props.paymentMethodChoice === PaymentMethod.ethereum, - ), - () => ( - - ), - ) - .otherwise(({ reverseRecord, seconds, paymentMethodChoice, durationType, callback }) => ( - - )) -} - -export type PricingProps = { - name: string - gracePeriodEndDate: Date | undefined - beautifiedName: string - - resolverExists: boolean | undefined - callback: (props: RegistrationStepData['pricing']) => void - isPrimaryLoading: boolean - hasPrimaryName: boolean - registrationData: RegistrationReducerDataItem - moonpayTransactionStatus?: MoonpayTransactionStatus - initiateMoonpayRegistrationMutation: ReturnType< - typeof useMoonpayRegistration - >['initiateMoonpayRegistrationMutation'] -} - -const minSeconds = 28 * ONE_DAY - -const Pricing = ({ - name, - gracePeriodEndDate, - beautifiedName, - callback, - isPrimaryLoading, - hasPrimaryName, - registrationData, - resolverExists, - moonpayTransactionStatus, - initiateMoonpayRegistrationMutation, -}: PricingProps) => { - const { t } = useTranslation('register') - - const { address } = useAccountSafely() - const { data: balance } = useBalance({ address }) - const resolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) - - const [seconds, setSeconds] = useState(() => registrationData.seconds ?? ONE_YEAR) - const [durationType, setDurationType] = useState<'date' | 'years'>( - registrationData.durationType ?? 'years', - ) - - const [reverseRecord, setReverseRecord] = useState(() => - registrationData.started ? registrationData.reverseRecord : !hasPrimaryName, - ) - - const hasPendingMoonpayTransaction = moonpayTransactionStatus === 'pending' - const hasFailedMoonpayTransaction = moonpayTransactionStatus === 'failed' - - const previousMoonpayTransactionStatus = usePrevious(moonpayTransactionStatus) - - const [paymentMethodChoice, setPaymentMethodChoice] = useState( - hasPendingMoonpayTransaction ? PaymentMethod.moonpay : PaymentMethod.ethereum, - ) - - // Keep radio button choice up to date - useEffect(() => { - if (moonpayTransactionStatus) { - setPaymentMethodChoice( - hasPendingMoonpayTransaction || hasFailedMoonpayTransaction - ? PaymentMethod.moonpay - : PaymentMethod.ethereum, - ) - } - }, [ - hasFailedMoonpayTransaction, - hasPendingMoonpayTransaction, - moonpayTransactionStatus, - previousMoonpayTransactionStatus, - setPaymentMethodChoice, - ]) - - const fullEstimate = useEstimateFullRegistration({ - name, - registrationData: { - ...registrationData, - reverseRecord, - seconds, - records: [{ key: 'ETH', value: resolverAddress, type: 'addr', group: 'address' }], - clearRecords: resolverExists, - resolverAddress, - }, - }) - - const { hasPremium, premiumFee, gasPrice, yearlyFee, totalDurationBasedFee, estimatedGasFee } = - fullEstimate - const durationRequiredBalance = totalDurationBasedFee ? (totalDurationBasedFee * 110n) / 100n : 0n - const totalRequiredBalance = durationRequiredBalance - ? durationRequiredBalance + (premiumFee || 0n) + (estimatedGasFee || 0n) - : 0n - - const previousYearlyFee = usePreviousDistinct(yearlyFee) || 0n - - const unsafeDisplayYearlyFee = yearlyFee === 0n ? previousYearlyFee : yearlyFee - - const showPaymentChoice = !isPrimaryLoading && address - - const previousEstimatedGasFee = usePreviousDistinct(estimatedGasFee) || 0n - - const unsafeDisplayEstimatedGasFee = - estimatedGasFee === 0n ? previousEstimatedGasFee : estimatedGasFee - - return ( - - {t('heading', { name: beautifiedName })} - - - {hasPremium && gracePeriodEndDate ? ( - - ) : ( - !!unsafeDisplayYearlyFee && - !!unsafeDisplayEstimatedGasFee && - !!gasPrice && ( - - ) - )} - {showPaymentChoice && ( - - )} - - - - - ) -} - -export default Pricing diff --git a/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts b/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts deleted file mode 100644 index 769876eae..000000000 --- a/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { useMutation } from '@tanstack/react-query' -import { useState } from 'react' -import { labelhash } from 'viem' -import { useChainId } from 'wagmi' - -import { useAccountSafely } from '@app/hooks/account/useAccountSafely' -import { useQueryOptions } from '@app/hooks/useQueryOptions' -import useRegistrationReducer from '@app/hooks/useRegistrationReducer' -import { MOONPAY_WORKER_URL } from '@app/utils/constants' -import { useQuery } from '@app/utils/query/useQuery' -import { getLabelFromName } from '@app/utils/utils' - -import { MoonpayTransactionStatus, SelectedItemProperties } from './types' - -export const useMoonpayRegistration = ( - dispatch: ReturnType['dispatch'], - normalisedName: string, - selected: SelectedItemProperties, - item: ReturnType['item'], -) => { - const chainId = useChainId() - const { address } = useAccountSafely() - const [hasMoonpayModal, setHasMoonpayModal] = useState(false) - const [moonpayUrl, setMoonpayUrl] = useState('') - const [isCompleted, setIsCompleted] = useState(false) - const currentExternalTransactionId = item.externalTransactionId - - const initiateMoonpayRegistrationMutation = useMutation({ - mutationFn: async (duration: number = 1) => { - const label = getLabelFromName(normalisedName) - const tokenId = labelhash(label) - - const requestUrl = `${ - MOONPAY_WORKER_URL[chainId] - }/signedurl?tokenId=${tokenId}&name=${encodeURIComponent( - normalisedName, - )}&duration=${duration}&walletAddress=${address}` - const response = await fetch(requestUrl) - const textResponse = await response.text() - setMoonpayUrl(textResponse) - - const params = new URLSearchParams(textResponse) - const externalTransactionId = params.get('externalTransactionId') || '' - - dispatch({ - name: 'setExternalTransactionId', - externalTransactionId, - selected, - }) - setHasMoonpayModal(true) - }, - }) - - const { queryKey } = useQueryOptions({ - params: { externalTransactionId: currentExternalTransactionId }, - functionName: 'getMoonpayStatus', - queryDependencyType: 'standard', - keyOnly: true, - }) - - // Monitor current transaction - const { data: transactionData } = useQuery({ - queryKey, - // TODO: refactor this func and pull query fn out of the hook - queryFn: async ({ queryKey: [{ externalTransactionId }] }) => { - const response = await fetch( - `${MOONPAY_WORKER_URL[chainId]}/transactionInfo?externalTransactionId=${externalTransactionId}`, - ) - const jsonResult = (await response.json()) as Array<{ status: MoonpayTransactionStatus }> - const result = jsonResult?.[0] - - if (result?.status === 'completed' && !isCompleted) { - setIsCompleted(true) - setHasMoonpayModal(false) - dispatch({ - name: 'moonpayTransactionCompleted', - selected, - }) - } - - return result || {} - }, - refetchOnWindowFocus: true, - refetchOnMount: true, - refetchInterval: 1000, - refetchIntervalInBackground: true, - enabled: !!currentExternalTransactionId && !isCompleted, - }) - - return { - moonpayUrl, - initiateMoonpayRegistrationMutation, - hasMoonpayModal, - setHasMoonpayModal, - currentExternalTransactionId, - moonpayTransactionStatus: transactionData?.status as MoonpayTransactionStatus, - } -} diff --git a/src/components/pages/profile/[name]/tabs/MoreTab/Miscellaneous/RegistrationDate.tsx b/src/components/pages/profile/[name]/tabs/MoreTab/Miscellaneous/RegistrationDate.tsx index 3579be60f..a43f58df3 100644 --- a/src/components/pages/profile/[name]/tabs/MoreTab/Miscellaneous/RegistrationDate.tsx +++ b/src/components/pages/profile/[name]/tabs/MoreTab/Miscellaneous/RegistrationDate.tsx @@ -1,10 +1,10 @@ import { useTranslation } from 'react-i18next' +import { useChainId } from 'wagmi' import { OutlinkSVG, Typography } from '@ensdomains/thorin' -import { useChainName } from '@app/hooks/chain/useChainName' import type useRegistrationDate from '@app/hooks/useRegistrationData' -import { formatDateTime, formatExpiry, makeEtherscanLink } from '@app/utils/utils' +import { createEtherscanLink, formatDateTime, formatExpiry } from '@app/utils/utils' import { DateLayout } from './components/DateLayout' @@ -14,7 +14,7 @@ export const RegistrationDate = ({ registrationData: ReturnType['data'] }) => { const { t } = useTranslation('common') - const chainName = useChainName() + const chainId = useChainId() if (!registrationData) return null return ( @@ -23,7 +23,7 @@ export const RegistrationDate = ({ {formatDateTime(registrationData.registrationDate)} diff --git a/src/components/pages/profile/[name]/tabs/MoreTab/Ownership.tsx b/src/components/pages/profile/[name]/tabs/MoreTab/Ownership.tsx index ff1f8c2c0..cbfc7f905 100644 --- a/src/components/pages/profile/[name]/tabs/MoreTab/Ownership.tsx +++ b/src/components/pages/profile/[name]/tabs/MoreTab/Ownership.tsx @@ -16,9 +16,8 @@ import { useDnsImportData } from '@app/hooks/ensjs/dns/useDnsImportData' import { GetDnsOwnerQueryKey, UseDnsOwnerError } from '@app/hooks/ensjs/dns/useDnsOwner' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import { useOwners } from '@app/hooks/useOwners' -import { makeIntroItem } from '@app/transaction-flow/intro' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { useTransactionManager } from '@app/transaction/transactionManager' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' import { OwnerItem } from '@app/types' import { shortenAddress } from '@app/utils/utils' @@ -206,8 +205,8 @@ const DNSOwnerSection = ({ }) => { const { address } = useAccount() const { t } = useTranslation('profile') - const { createTransactionFlow } = useTransactionFlow() const queryClient = useQueryClient() + const startFlow = useTransactionManager((s) => s.startFlow) const canShow = useMemo(() => { let hasMatchingAddress = false @@ -234,17 +233,27 @@ const DNSOwnerSection = ({ const handleSyncManager = () => { const currentManager = owners.find((owner) => owner.label === 'name.manager') - createTransactionFlow(`sync-manager-${name}-${address}`, { + startFlow({ + flowId: `sync-manager-${name}-${address}`, intro: { title: ['tabs.more.ownership.dnsOwnerWarning.syncManager', { ns: 'profile' }], - content: makeIntroItem('SyncManager', { isWrapped, manager: currentManager!.address }), + content: { + name: 'SyncManager', + data: { + isWrapped, + manager: currentManager!.address, + }, + }, }, transactions: [ - createTransactionItem('syncManager', { - address: address!, - name, - dnsImportData: dnsImportData!, - }), + { + name: 'syncManager', + data: { + address: address!, + name, + dnsImportData: dnsImportData!, + }, + }, ], }) } @@ -301,7 +310,6 @@ const Ownership = ({ }) => { const { t } = useTranslation('profile') - const { usePreparedDataInput } = useTransactionFlow() const showSendNameInput = usePreparedDataInput('SendName') const handleSend = () => { showSendNameInput(`send-name-${name}`, { diff --git a/src/components/pages/profile/[name]/tabs/MoreTab/Resolver.tsx b/src/components/pages/profile/[name]/tabs/MoreTab/Resolver.tsx index 881950839..733afab9d 100644 --- a/src/components/pages/profile/[name]/tabs/MoreTab/Resolver.tsx +++ b/src/components/pages/profile/[name]/tabs/MoreTab/Resolver.tsx @@ -8,7 +8,7 @@ import { cacheableComponentStyles } from '@app/components/@atoms/CacheableCompon import { DisabledButtonWithTooltip } from '@app/components/@molecules/DisabledButtonWithTooltip' import RecordItem from '@app/components/RecordItem' import { useResolver } from '@app/hooks/ensjs/public/useResolver' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' import { useBreakpoint } from '@app/utils/BreakpointProvider' import { emptyAddress } from '@app/utils/constants' import { useHasGraphError } from '@app/utils/SyncProvider/SyncProvider' @@ -97,7 +97,6 @@ const Resolver = ({ const { data: hasGraphError, isLoading: hasGraphErrorLoading } = useHasGraphError() - const { usePreparedDataInput } = useTransactionFlow() const showEditResolverInput = usePreparedDataInput('EditResolver') const handleEditClick = () => { showEditResolverInput(`resolver-upgrade-${name}`, { diff --git a/src/components/pages/profile/[name]/tabs/MoreTab/Token/Token.tsx b/src/components/pages/profile/[name]/tabs/MoreTab/Token/Token.tsx index 95d72bf51..a555aea80 100644 --- a/src/components/pages/profile/[name]/tabs/MoreTab/Token/Token.tsx +++ b/src/components/pages/profile/[name]/tabs/MoreTab/Token/Token.tsx @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { labelhash, namehash } from 'viem' +import { useChainId } from 'wagmi' import { mq, Tag, Typography } from '@ensdomains/thorin' @@ -8,9 +9,8 @@ import { CacheableComponent } from '@app/components/@atoms/CacheableComponent' import { NFTWithPlaceholder } from '@app/components/NFTWithPlaceholder' import { Outlink } from '@app/components/Outlink' import RecordItem from '@app/components/RecordItem' -import { useChainName } from '@app/hooks/chain/useChainName' import { useContractAddress } from '@app/hooks/chain/useContractAddress' -import { checkETH2LDFromName, makeEtherscanLink } from '@app/utils/utils' +import { checkETH2LDFromName, createEtherscanLink } from '@app/utils/utils' import { TabWrapper } from '../../../../TabWrapper' @@ -107,7 +107,7 @@ const NftBox = styled(NFTWithPlaceholder)( const Token = ({ name, isWrapped }: Props) => { const { t } = useTranslation('profile') - const networkName = useChainName() + const chainId = useChainId() const nameWrapperAddress = useContractAddress({ contract: 'ensNameWrapper' }) const registrarAddress = useContractAddress({ contract: 'ensBaseRegistrarImplementation' }) @@ -127,7 +127,11 @@ const Token = ({ name, isWrapped }: Props) => { {hasToken ? ( {t('etherscan', { ns: 'common' })} diff --git a/src/components/pages/profile/[name]/tabs/MoreTab/Token/UnwrapButton.tsx b/src/components/pages/profile/[name]/tabs/MoreTab/Token/UnwrapButton.tsx index fedbb1c7e..4b86dde76 100644 --- a/src/components/pages/profile/[name]/tabs/MoreTab/Token/UnwrapButton.tsx +++ b/src/components/pages/profile/[name]/tabs/MoreTab/Token/UnwrapButton.tsx @@ -4,8 +4,7 @@ import { GetOwnerReturnType } from '@ensdomains/ensjs/public' import { useAccountSafely } from '@app/hooks/account/useAccountSafely' import { NameWrapperState } from '@app/hooks/fuses/useFusesStates' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { useTransactionManager } from '@app/transaction/transactionManager' import BaseWrapButton from './BaseWrapButton' @@ -20,10 +19,11 @@ const UnwrapButton = ({ name, ownerData, status, disabled }: Props) => { const { t } = useTranslation('profile') const { address } = useAccountSafely() - const { createTransactionFlow } = useTransactionFlow() + const startFlow = useTransactionManager((s) => s.startFlow) const handleUnwrapClick = () => { - createTransactionFlow(`unwrapName-${name}`, { - transactions: [createTransactionItem('unwrapName', { name })], + startFlow({ + flowId: `unwrapName-${name}`, + transactions: [{ name: 'unwrapName', data: { name } }], }) } diff --git a/src/components/pages/profile/[name]/tabs/MoreTab/Token/WrapButton.tsx b/src/components/pages/profile/[name]/tabs/MoreTab/Token/WrapButton.tsx index f442f69b7..d7d404149 100644 --- a/src/components/pages/profile/[name]/tabs/MoreTab/Token/WrapButton.tsx +++ b/src/components/pages/profile/[name]/tabs/MoreTab/Token/WrapButton.tsx @@ -6,10 +6,10 @@ import { checkIsDecrypted } from '@ensdomains/ensjs/utils' import { useAccountSafely } from '@app/hooks/account/useAccountSafely' import { useResolverStatus } from '@app/hooks/resolver/useResolverStatus' import { useWrapperApprovedForAll } from '@app/hooks/useWrapperApprovedForAll' -import { makeIntroItem } from '@app/transaction-flow/intro' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' -import { GenericTransaction, TransactionFlowItem } from '@app/transaction-flow/types' +import type { FlowInitialiserData } from '@app/transaction/slices/createFlowSlice' +import { useTransactionManager } from '@app/transaction/transactionManager' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' +import { createUserTransaction } from '@app/transaction/user/transaction' import { Profile } from '@app/types' import { useHasGraphError } from '@app/utils/SyncProvider/SyncProvider' @@ -44,59 +44,66 @@ const WrapButton = ({ name, ownerData, profile, canBeWrapped, isManager, isRegis canBeWrapped, }) - const { createTransactionFlow, resumeTransactionFlow, getResumable, usePreparedDataInput } = - useTransactionFlow() + const flowId = `wrapName-${name}` + + const getResumable = useTransactionManager((s) => s.isFlowResumable) + const resumeFlow = useTransactionManager((s) => s.resumeFlow) + const startFlow = useTransactionManager((s) => s.startFlow) + const showUnknownLabelsInput = usePreparedDataInput('UnknownLabels') - const resumable = getResumable(`wrapName-${name}`) + const resumable = getResumable(flowId) const handleWrapClick = () => { if (!hasOwnerData) return - if (resumable) return resumeTransactionFlow(`wrapName-${name}`) + if (resumable) return resumeFlow(flowId) const isManagerAndShouldMigrate = isManager && shouldMigrate const isRegistrantAndShouldMigrate = !isManager && isRegistrant && shouldMigrate const needsApproval = isManager && isSubname && !approvedForAll - const transactions: GenericTransaction[] = [ + const transactions = [ ...(needsApproval ? [ - createTransactionItem('approveNameWrapper', { + createUserTransaction('approveNameWrapper', { address: address!, }), ] : []), ...(isManagerAndShouldMigrate ? [ - createTransactionItem('migrateProfile', { + createUserTransaction('migrateProfile', { name, }), ] : []), - createTransactionItem('wrapName', { + createUserTransaction('wrapName', { name, }), ...(isRegistrantAndShouldMigrate - ? [createTransactionItem('migrateProfile', { name, resolverAddress })] + ? [createUserTransaction('migrateProfile', { name, resolverAddress })] : []), ] - const transactionFlowItem: TransactionFlowItem = { + const flow = { + flowId, transactions, resumable: true, intro: { title: ['details.wrap.startTitle', { ns: 'profile' }], - content: makeIntroItem('WrapName', { name }), + content: { + name: 'WrapName', + data: { name }, + }, }, - } + } satisfies FlowInitialiserData const key = `wrapName-${name}` if (!checkIsDecrypted(name)) return showUnknownLabelsInput(key, { name, - key, - transactionFlowItem, + flow, }) - return createTransactionFlow(key, transactionFlowItem) + return startFlow(flow) } const isLoading = isApprovalLoading || resolverStatus.isLoading || hasGraphErrorLoading diff --git a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ContractSection/ContractSection.tsx b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ContractSection/ContractSection.tsx index 0e29aeca4..c5d99720b 100644 --- a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ContractSection/ContractSection.tsx +++ b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ContractSection/ContractSection.tsx @@ -1,12 +1,12 @@ import { useTranslation } from 'react-i18next' +import { useChainId } from 'wagmi' import { Card, Helper, RecordItem } from '@ensdomains/thorin' -import { useChainName } from '@app/hooks/chain/useChainName' import { useContractAddress } from '@app/hooks/chain/useContractAddress' import type { useNameDetails } from '@app/hooks/useNameDetails' import { useBreakpoint } from '@app/utils/BreakpointProvider' -import { makeEtherscanLink } from '@app/utils/utils' +import { createEtherscanLink } from '@app/utils/utils' import { Header } from './components/Header' @@ -17,7 +17,7 @@ type Props = { export const ContractSection = ({ details }: Props) => { const { t } = useTranslation('profile') const address = useContractAddress({ contract: 'ensNameWrapper' }) - const chainName = useChainName() + const chainId = useChainId() const breakpoint = useBreakpoint() const { isLoading } = details @@ -26,7 +26,11 @@ export const ContractSection = ({ details }: Props) => { return (
- + {address} diff --git a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/hooks/useExpiryActions.tsx b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/hooks/useExpiryActions.tsx index e18995637..a736fb40b 100644 --- a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/hooks/useExpiryActions.tsx +++ b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/hooks/useExpiryActions.tsx @@ -5,7 +5,7 @@ import { useAccount } from 'wagmi' import { GetOwnerReturnType, GetWrapperDataReturnType } from '@ensdomains/ensjs/public' import { CalendarSVG, FastForwardSVG } from '@ensdomains/thorin' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' import { nameLevel } from '@app/utils/name' import type { useExpiryDetails } from './useExpiryDetails' @@ -35,7 +35,6 @@ export const useExpiryActions = ({ }) => { const { t } = useTranslation('common') const { address } = useAccount() - const { usePreparedDataInput } = useTransactionFlow() const showExtendNamesInput = usePreparedDataInput('ExtendNames') // TODO: remove this when we add support for extending wrapped subnames diff --git a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/hooks/useExpiryDetails.ts b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/hooks/useExpiryDetails.ts index 4bde1eb79..5a5dd8c33 100644 --- a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/hooks/useExpiryDetails.ts +++ b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/hooks/useExpiryDetails.ts @@ -1,8 +1,8 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { match, P } from 'ts-pattern' +import { useChainId } from 'wagmi' -import { useChainName } from '@app/hooks/chain/useChainName' import { useNameType } from '@app/hooks/nameType/useNameType' import { useBasicName } from '@app/hooks/useBasicName' import type { useNameDetails } from '@app/hooks/useNameDetails' @@ -11,7 +11,7 @@ import { GRACE_PERIOD } from '@app/utils/constants' import { safeDateObj } from '@app/utils/date' import { parentName } from '@app/utils/name' import { getSupportLink } from '@app/utils/supportLinks' -import { checkETH2LDFromName, makeEtherscanLink } from '@app/utils/utils' +import { checkETH2LDFromName, createEtherscanLink } from '@app/utils/utils' type Input = { name: string @@ -42,7 +42,7 @@ export const useExpiryDetails = ({ name, details }: Input, options: Options = {} name: parentName(name), enabled: enabled && !!nameType.data && nameType.data!.includes('subname'), }) - const chainName = useChainName() + const chainId = useChainId() const registrationData = useRegistrationData({ name, enabled: enabled && isETH2LD }) const isLoading = @@ -89,7 +89,10 @@ export const useExpiryDetails = ({ name, details }: Input, options: Options = {} { type: 'registration', date: registrationData?.data?.registrationDate, - link: makeEtherscanLink(registrationData?.data?.transactionHash!, chainName), + link: createEtherscanLink({ + data: registrationData?.data?.transactionHash!, + chainId, + }), }, ] : []), @@ -160,7 +163,7 @@ export const useExpiryDetails = ({ name, details }: Input, options: Options = {} parentData.wrapperData, parentData.expiryDate, registrationData.data, - chainName, + chainId, ], ) diff --git a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/components/RoleRow.tsx b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/components/RoleRow.tsx index b96775d79..ac99c16d4 100644 --- a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/components/RoleRow.tsx +++ b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/components/RoleRow.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { useCopyToClipboard } from 'react-use' import styled, { css } from 'styled-components' import { Address } from 'viem' +import { useChainId } from 'wagmi' import { Button, @@ -15,13 +16,12 @@ import { } from '@ensdomains/thorin' import { AvatarWithIdentifier } from '@app/components/@molecules/AvatarWithIdentifier/AvatarWithIdentifier' -import { useChainName } from '@app/hooks/chain/useChainName' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import type { Role } from '@app/hooks/ownership/useRoles/useRoles' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { getDestination } from '@app/routes' import { emptyAddress } from '@app/utils/constants' -import { checkETH2LDFromName, makeEtherscanLink } from '@app/utils/utils' +import { checkETH2LDFromName, createEtherscanLink } from '@app/utils/utils' import { useRoleActions } from '../hooks/useRoleActions' import { RoleTag } from './RoleTag' @@ -69,7 +69,7 @@ export const RoleRow = ({ name, address, roles, actions, isWrapped, isEmancipate const { t } = useTranslation('common') const primary = usePrimaryName({ address: address!, enabled: !!address }) - const networkName = useChainName() + const chainId = useChainId() const [, copy] = useCopyToClipboard() const etherscanAction = useMemo(() => { @@ -80,10 +80,11 @@ export const RoleRow = ({ name, address, roles, actions, isWrapped, isEmancipate if (!hasToken) return null return { label: t('transaction.viewEtherscan', { ns: 'common' }), - onClick: () => window.open(makeEtherscanLink(address!, networkName, 'address'), '_blank'), + onClick: () => + window.open(createEtherscanLink({ data: address!, chainId, route: 'address' }), '_blank'), icon: , } - }, [primary.data?.name, isWrapped, t, address, networkName]) + }, [primary.data?.name, isWrapped, t, address, chainId]) const editRolesAction = actions?.find(({ type, disabled }) => type === 'edit-roles' && !disabled) diff --git a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/hooks/useRoleActions.test.tsx b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/hooks/useRoleActions.test.tsx index 5d74e3115..9faa63486 100644 --- a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/hooks/useRoleActions.test.tsx +++ b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/hooks/useRoleActions.test.tsx @@ -22,12 +22,12 @@ vi.mock('@app/hooks/account/useAccountSafely', () => ({ })) const mockCheckCanSend = vi.fn() -vi.mock('@app/transaction-flow/input/SendName/utils/checkCanSend', () => ({ +vi.mock('@app/transaction/user/SendName/utils/checkCanSend', () => ({ checkCanSend: () => mockCheckCanSend(), })) const mockCheckCanSyncManager = vi.fn() -vi.mock('@app/transaction-flow/input/SyncManager/utils/checkCanSyncManager', () => ({ +vi.mock('@app/transaction/user/SyncManager/utils/checkCanSyncManager', () => ({ checkCanSyncManager: () => mockCheckCanSyncManager(), })) diff --git a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/hooks/useRoleActions.tsx b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/hooks/useRoleActions.tsx index 804434f7c..fc46847ae 100644 --- a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/hooks/useRoleActions.tsx +++ b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/RolesSection/hooks/useRoleActions.tsx @@ -17,9 +17,9 @@ import { useNameType } from '@app/hooks/nameType/useNameType' import type { GroupedRoleRecord } from '@app/hooks/ownership/useRoles/useRoles' import { getAvailableRoles } from '@app/hooks/ownership/useRoles/utils/getAvailableRoles' import type { useNameDetails } from '@app/hooks/useNameDetails' -import { checkCanSend } from '@app/transaction-flow/input/SendName/utils/checkCanSend' -import { checkCanSyncManager } from '@app/transaction-flow/input/SyncManager/utils/checkCanSyncManager' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' +import { checkCanSend } from '@app/transaction/user/input/SendName/utils/checkCanSend' +import { checkCanSyncManager } from '@app/transaction/user/input/SyncManager/utils/checkCanSyncManager' type Action = Omit & { primary?: boolean @@ -42,7 +42,6 @@ export const useRoleActions = ({ name, roles, details }: Props) => { const abilities = useAbilities({ name }) const queryClient = useQueryClient() - const { usePreparedDataInput } = useTransactionFlow() const showSendNameInput = usePreparedDataInput('SendName') const showEditRolesInput = usePreparedDataInput('EditRoles') const showSyncManagerInput = usePreparedDataInput('SyncManager') diff --git a/src/components/pages/profile/[name]/tabs/PermissionsTab/ExpiryPermissions.tsx b/src/components/pages/profile/[name]/tabs/PermissionsTab/ExpiryPermissions.tsx index 0a515ccf1..6cdec21d5 100644 --- a/src/components/pages/profile/[name]/tabs/PermissionsTab/ExpiryPermissions.tsx +++ b/src/components/pages/profile/[name]/tabs/PermissionsTab/ExpiryPermissions.tsx @@ -6,7 +6,7 @@ import { Button, Typography } from '@ensdomains/thorin' import type { useFusesSetDates } from '@app/hooks/fuses/useFusesSetDates' import type { useFusesStates } from '@app/hooks/fuses/useFusesStates' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' import { Section, SectionFooter, SectionItem } from './Section' @@ -44,7 +44,6 @@ export const ExpiryPermissions = ({ parentExpiry, }: Props) => { const { t } = useTranslation('profile') - const { usePreparedDataInput } = useTransactionFlow() const showRevokePermissionsInput = usePreparedDataInput('RevokePermissions') const handleRevokePermissions = () => { diff --git a/src/components/pages/profile/[name]/tabs/PermissionsTab/NameChangePermissions.tsx b/src/components/pages/profile/[name]/tabs/PermissionsTab/NameChangePermissions.tsx index f956bab92..45174f4df 100644 --- a/src/components/pages/profile/[name]/tabs/PermissionsTab/NameChangePermissions.tsx +++ b/src/components/pages/profile/[name]/tabs/PermissionsTab/NameChangePermissions.tsx @@ -8,7 +8,7 @@ import { Button, Typography } from '@ensdomains/thorin' import type { useFusesSetDates } from '@app/hooks/fuses/useFusesSetDates' import type { useFusesStates } from '@app/hooks/fuses/useFusesStates' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' import { DisabledButtonWithTooltip } from '../../../../../@molecules/DisabledButtonWithTooltip' import { Section, SectionFooter, SectionItem } from './Section' @@ -86,7 +86,6 @@ export const NameChangePermissions = ({ canEditPermissions, }: Props) => { const { t } = useTranslation('profile') - const { usePreparedDataInput } = useTransactionFlow() const showRevokePermissionsInput = usePreparedDataInput('RevokePermissions') const isParentLocked = parentState === 'locked' || wrapperData?.fuses?.parent.IS_DOT_ETH diff --git a/src/components/pages/profile/[name]/tabs/PermissionsTab/OwnershipPermissions.tsx b/src/components/pages/profile/[name]/tabs/PermissionsTab/OwnershipPermissions.tsx index e9e914fc1..2f07f0d3d 100644 --- a/src/components/pages/profile/[name]/tabs/PermissionsTab/OwnershipPermissions.tsx +++ b/src/components/pages/profile/[name]/tabs/PermissionsTab/OwnershipPermissions.tsx @@ -8,7 +8,7 @@ import { Button, Typography } from '@ensdomains/thorin' import { StyledLink } from '@app/components/@atoms/StyledLink' import type { useFusesSetDates } from '@app/hooks/fuses/useFusesSetDates' import type { useFusesStates } from '@app/hooks/fuses/useFusesStates' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' import { Section, SectionFooter, SectionItem, SectionList } from './Section' @@ -50,7 +50,6 @@ export const OwnershipPermissions = ({ }: Props) => { const { t } = useTranslation('profile') - const { usePreparedDataInput } = useTransactionFlow() const showRevokePermissionsInput = usePreparedDataInput('RevokePermissions') const nameParts = name.split('.') diff --git a/src/components/pages/profile/[name]/tabs/RecordsTab.tsx b/src/components/pages/profile/[name]/tabs/RecordsTab.tsx index bc51cfa22..c70549d31 100644 --- a/src/components/pages/profile/[name]/tabs/RecordsTab.tsx +++ b/src/components/pages/profile/[name]/tabs/RecordsTab.tsx @@ -9,7 +9,7 @@ import { cacheableComponentStyles } from '@app/components/@atoms/CacheableCompon import { DisabledButtonWithTooltip } from '@app/components/@molecules/DisabledButtonWithTooltip' import { Outlink } from '@app/components/Outlink' import RecordItem from '@app/components/RecordItem' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' import { AddressRecord, Profile, TextRecord } from '@app/types' import { abiDisplayValue } from '@app/utils/abi' import { emptyAddress } from '@app/utils/constants' @@ -153,7 +153,6 @@ export const RecordsTab = ({ } }, [name, chainId, contentHash]) - const { usePreparedDataInput } = useTransactionFlow() const showAdvancedEditorInput = usePreparedDataInput('AdvancedEditor') const handleShowEditor = () => showAdvancedEditorInput(`advanced-editor-${name}`, { name }, { disableBackgroundClick: true }) diff --git a/src/components/pages/profile/[name]/tabs/SubnamesTab.tsx b/src/components/pages/profile/[name]/tabs/SubnamesTab.tsx index 4fd3f918d..076eb1a4a 100644 --- a/src/components/pages/profile/[name]/tabs/SubnamesTab.tsx +++ b/src/components/pages/profile/[name]/tabs/SubnamesTab.tsx @@ -16,7 +16,7 @@ import { Card } from '@app/components/Card' import { Outlink } from '@app/components/Outlink' import { TabWrapper } from '@app/components/pages/profile/TabWrapper' import { useSubnames } from '@app/hooks/ensjs/subgraph/useSubnames' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' import { emptyAddress } from '@app/utils/constants' import { getSupportLink } from '@app/utils/supportLinks' @@ -113,7 +113,6 @@ export const SubnamesTab = ({ }) => { const { t } = useTranslation('profile') const { address } = useAccount() - const { usePreparedDataInput } = useTransactionFlow() const showCreateSubnameInput = usePreparedDataInput('CreateSubname') const [sortType, setSortType] = useQueryParameterState< diff --git a/src/components/pages/profile/settings/DevSection.tsx b/src/components/pages/profile/settings/DevSection.tsx index 311683a00..5d542caa9 100644 --- a/src/components/pages/profile/settings/DevSection.tsx +++ b/src/components/pages/profile/settings/DevSection.tsx @@ -6,16 +6,14 @@ import { mine, setAutomine, } from 'viem/actions' -import { Config, useClient, useSendTransaction } from 'wagmi' +import { Config, useClient } from 'wagmi' import { Button } from '@ensdomains/thorin' import { localhostWithEns } from '@app/constants/chains' -import { useAddRecentTransaction } from '@app/hooks/transactions/useAddRecentTransaction' import { useLocalStorage } from '@app/hooks/useLocalStorage' -import { DetailedSwitch } from '@app/transaction-flow/input/ProfileEditor/components/DetailedSwitch' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { useTransactionManager } from '@app/transaction/transactionManager' +import { DetailedSwitch } from '@app/transaction/user/input/ProfileEditor/components/DetailedSwitch' import { SectionContainer } from './Section' @@ -42,39 +40,19 @@ export const DevSection = () => { const client = useClient() const testClient = useMemo(() => ({ ...client, mode: 'anvil' }) as const, [client]) - const addTransaction = useAddRecentTransaction() - const { createTransactionFlow } = useTransactionFlow() - const { sendTransactionAsync } = useSendTransaction() + const startFlow = useTransactionManager((s) => s.startFlow) - const addSuccess = async () => { - const hash = await sendTransactionAsync({ - to: '0x0000000000000000000000000000000000000000', - value: 0n, - gas: 21000n, - }) - addTransaction({ - hash, - action: 'test', - searchRetries: 0, + const sendName = () => { + startFlow({ + flowId: 'dev-sendName', + transactions: [{ name: 'testSendName', data: {} }], }) } - const sendName = async () => { - createTransactionFlow('dev-sendName', { - transactions: [createTransactionItem('testSendName', {})], - }) - } - - const addFailure = async () => { - const hash = await sendTransactionAsync({ - to: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', - data: '0x1231237123423423', - gas: 1000000n, - }) - addTransaction({ - hash, - action: 'test', - searchRetries: 0, + const addFailure = () => { + startFlow({ + flowId: 'dev-addFailure', + transactions: [{ name: '__dev_failure', data: {} }], }) } @@ -98,9 +76,8 @@ export const DevSection = () => { return ( - {process.env.NEXT_PUBLIC_PROVIDER && ( + {true && ( <> - diff --git a/src/components/pages/profile/settings/PrimarySection.tsx b/src/components/pages/profile/settings/PrimarySection.tsx index ff4adc7dd..dc6ba993a 100644 --- a/src/components/pages/profile/settings/PrimarySection.tsx +++ b/src/components/pages/profile/settings/PrimarySection.tsx @@ -8,7 +8,7 @@ import { DisabledButtonWithTooltip } from '@app/components/@molecules/DisabledBu import { useAccountSafely } from '@app/hooks/account/useAccountSafely' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import { useBasicName } from '@app/hooks/useBasicName' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' import { useHasGraphError } from '@app/utils/SyncProvider/SyncProvider' const SkeletonFiller = styled.div( @@ -133,7 +133,6 @@ export const PrimarySection = () => { const { t } = useTranslation('settings') const { address } = useAccountSafely() - const { usePreparedDataInput } = useTransactionFlow() const showSelectPrimaryNameInput = usePreparedDataInput('SelectPrimaryName') const showResetPrimaryNameInput = usePreparedDataInput('ResetPrimaryName') diff --git a/src/components/pages/profile/settings/TransactionSection/ClearTransactionsDialog.tsx b/src/components/pages/profile/settings/TransactionSection/ClearTransactionsDialog.tsx index 1e1ab1f5e..86d0f750d 100644 --- a/src/components/pages/profile/settings/TransactionSection/ClearTransactionsDialog.tsx +++ b/src/components/pages/profile/settings/TransactionSection/ClearTransactionsDialog.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { Button, Dialog } from '@ensdomains/thorin' -import { CenteredTypography } from '@app/transaction-flow/input/ProfileEditor/components/CenteredTypography' +import { CenteredTypography } from '@app/transaction/user/input/ProfileEditor/components/CenteredTypography' type Props = { onClear: () => void } & Omit, 'children' | 'variant'> diff --git a/src/components/pages/profile/settings/TransactionSection/TransactionSection.tsx b/src/components/pages/profile/settings/TransactionSection/TransactionSection.tsx index 013aa29cf..18db839cb 100644 --- a/src/components/pages/profile/settings/TransactionSection/TransactionSection.tsx +++ b/src/components/pages/profile/settings/TransactionSection/TransactionSection.tsx @@ -1,17 +1,17 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import type { Hash } from 'viem' import { Button, mq, Spinner, Typography } from '@ensdomains/thorin' import { Card } from '@app/components/Card' import { Outlink } from '@app/components/Outlink' -import { useChainName } from '@app/hooks/chain/useChainName' -import { useClearRecentTransactions } from '@app/hooks/transactions/useClearRecentTransactions' -import { useRecentTransactions } from '@app/hooks/transactions/useRecentTransactions' import useThrottledCallback from '@app/hooks/useThrottledCallback' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' -import { makeEtherscanLink } from '@app/utils/utils' +import type { StoredTransaction } from '@app/transaction/slices/createTransactionSlice' +import { useTransactionManager } from '@app/transaction/transactionManager' +import type { UserTransaction } from '@app/transaction/user/transaction' +import { createEtherscanLink } from '@app/utils/utils' import { SectionContainer } from '../Section' import { ClearTransactionsDialog } from './ClearTransactionsDialog' @@ -128,37 +128,34 @@ const InfoContainer = styled.div( `, ) -const getTransactionExtraInfo = (action: string, key?: string) => { - if (!key) return '' - if (action === 'registerName' || action === 'commitName') { - return `: ${key.replace(/^(?:register|commit)-(.*)-0x[a-fA-F0-9]{40}$/g, '$1')}` - } - return '' +const getTransactionExtraInfo = (transaction: UserTransaction) => { + if (transaction.name !== 'registerName' && transaction.name !== 'commitName') return '' + return `: ${transaction.data.name}` } export const TransactionSection = () => { const { t: tc } = useTranslation() const { t } = useTranslation('settings') - const chainName = useChainName() - const transactions = useRecentTransactions() - const clearTransactions = useClearRecentTransactions() const [viewAmt, setViewAmt] = useState(5) - const nonRepricedTransactions = transactions.filter((tx) => tx.status !== 'repriced') - - const visibleTransactions = nonRepricedTransactions.slice(0, viewAmt) + const transactions = useTransactionManager((s) => + s + .getAllTransactions() + .filter((tx): tx is Extract => !!tx.currentHash), + ) + const visibleTransactions = transactions.slice(0, viewAmt) - const canClear = useMemo(() => { - return nonRepricedTransactions.length > 0 - }, [nonRepricedTransactions.length]) + const canClear = transactions.length > 0 - const { getResumable, resumeTransactionFlow } = useTransactionFlow() + const clearAll = useTransactionManager((s) => s.clearTransactionsAndFlows) + const isTransactionResumable = useTransactionManager((s) => s.isTransactionResumable) + const resumeFlow = useTransactionManager((s) => s.resumeFlow) const ref = useRef(null) const [height, setHeight] = useState('auto') - const hasViewMore = nonRepricedTransactions.length > viewAmt + const hasViewMore = transactions.length > viewAmt const [width, _setWidth] = useState(0) const setWidth = useThrottledCallback(_setWidth, 300) @@ -177,7 +174,7 @@ export const TransactionSection = () => { useEffect(() => { const _height = ref.current?.getBoundingClientRect().height || 0 setHeight(_height ? `${_height}px` : 'auto') - }, [nonRepricedTransactions.length, hasViewMore, width]) + }, [transactions.length, hasViewMore, width]) const [showClearDialog, setShowClearDialog] = useState(false) @@ -201,15 +198,16 @@ export const TransactionSection = () => { > - {nonRepricedTransactions.length > 0 ? ( + {transactions.length > 0 ? ( <> - {visibleTransactions.map(({ hash, status, action, key }, i) => { - const resumable = key && getResumable(key) + {visibleTransactions.map((transaction) => { + const { currentHash, status, name, flowId } = transaction + const resumable = isTransactionResumable(transaction) return ( {status === 'pending' && ( @@ -217,11 +215,14 @@ export const TransactionSection = () => { )} {`${tc( - `transaction.description.${action}`, - )}${getTransactionExtraInfo(action, key)}`} + `transaction.description.${name}`, + )}${getTransactionExtraInfo(transaction)}`} {tc(`transaction.status.${status}.regular`)} @@ -230,7 +231,7 @@ export const TransactionSection = () => { {resumable && ( - @@ -260,7 +261,7 @@ export const TransactionSection = () => { onClose={() => setShowClearDialog(false)} onDismiss={() => setShowClearDialog(false)} onClear={() => { - clearTransactions() + clearAll() setShowClearDialog(false) setViewAmt(5) }} diff --git a/src/components/pages/profile/[name]/registration/FullInvoice.tsx b/src/components/pages/register/FullInvoice.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/FullInvoice.tsx rename to src/components/pages/register/FullInvoice.tsx diff --git a/src/components/pages/profile/[name]/registration/Registration.tsx b/src/components/pages/register/Registration.tsx similarity index 53% rename from src/components/pages/profile/[name]/registration/Registration.tsx rename to src/components/pages/register/Registration.tsx index 5b61385e8..766713222 100644 --- a/src/components/pages/profile/[name]/registration/Registration.tsx +++ b/src/components/pages/register/Registration.tsx @@ -1,31 +1,28 @@ import Head from 'next/head' -import { useCallback, useEffect, useMemo } from 'react' +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { match, P } from 'ts-pattern' -import { useAccount, useChainId } from 'wagmi' +import { useAccount } from 'wagmi' import { Dialog, Helper, mq, Typography } from '@ensdomains/thorin' import { BaseLinkWithHistory } from '@app/components/@atoms/BaseLink' import { InnerDialog } from '@app/components/@atoms/InnerDialog' -import { ProfileRecord } from '@app/constants/profileRecordOptions' import { useContractAddress } from '@app/hooks/chain/useContractAddress' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import { useNameDetails } from '@app/hooks/useNameDetails' -import useRegistrationReducer from '@app/hooks/useRegistrationReducer' import { useResolverExists } from '@app/hooks/useResolverExists' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { Content } from '@app/layouts/Content' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' -import { isLabelTooLong, secondsToYears } from '@app/utils/utils' +import { useTransactionManager } from '@app/transaction/transactionManager' +import { isLabelTooLong } from '@app/utils/utils' import Complete from './steps/Complete' import Info from './steps/Info' import Pricing from './steps/Pricing/Pricing' import Profile from './steps/Profile/Profile' import Transactions from './steps/Transactions' -import { BackObj, PaymentMethod, RegistrationStepData } from './types' import { useMoonpayRegistration } from './useMoonpayRegistration' const ViewProfileContainer = styled.div( @@ -106,13 +103,8 @@ const Registration = ({ nameDetails, isLoading }: Props) => { const { t } = useTranslation('register') const router = useRouterWithHistory() - const chainId = useChainId() const { address } = useAccount() const primary = usePrimaryName({ address }) - const selected = useMemo( - () => ({ name: nameDetails.normalisedName, address: address!, chainId }), - [address, chainId, nameDetails.normalisedName], - ) const { normalisedName, beautifiedName = '' } = nameDetails const defaultResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) const { data: resolverExists, isLoading: resolverExistsLoading } = useResolverExists({ @@ -121,126 +113,128 @@ const Registration = ({ nameDetails, isLoading }: Props) => { }) const labelTooLong = isLabelTooLong(normalisedName) - const { dispatch, item } = useRegistrationReducer(selected) - const step = item.queue[item.stepIndex] - - const keySuffix = `${nameDetails.normalisedName}-${address}` - const commitKey = `commit-${keySuffix}` - const registerKey = `register-${keySuffix}` - const { cleanupFlow } = useTransactionFlow() + const currentRegistrationFlowStep = useTransactionManager((s) => + s.getCurrentRegistrationFlowStep(normalisedName), + ) + const clearRegistrationFlow = useTransactionManager((s) => s.clearRegistrationFlow) + const moonpayUrl = useTransactionManager( + (s) => s.getCurrentRegistrationFlowOrDefault(normalisedName).externalTransactionData?.url, + ) const { - moonpayUrl, initiateMoonpayRegistrationMutation, hasMoonpayModal, setHasMoonpayModal, moonpayTransactionStatus, - } = useMoonpayRegistration(dispatch, normalisedName, selected, item) - - const pricingCallback = ({ - seconds, - reverseRecord, - paymentMethodChoice, - durationType, - }: RegistrationStepData['pricing']) => { - if (paymentMethodChoice === PaymentMethod.moonpay) { - initiateMoonpayRegistrationMutation.mutate(secondsToYears(seconds)) - return - } - dispatch({ - name: 'setPricingData', - payload: { seconds, reverseRecord, durationType }, - selected, - }) - if (!item.queue.includes('profile')) { - // if profile is not in queue, set the default profile data - dispatch({ - name: 'setProfileData', - payload: { - records: [{ key: 'eth', group: 'address', type: 'addr', value: address! }], - clearRecords: resolverExists, - resolverAddress: defaultResolverAddress, - }, - selected, - }) - if (reverseRecord) { - // if reverse record is selected, add the profile step to the queue - dispatch({ - name: 'setQueue', - payload: ['pricing', 'profile', 'info', 'transactions', 'complete'], - selected, - }) - } - } - - // If profile is in queue and reverse record is selected, make sure that eth record is included and is set to address - if (item.queue.includes('profile') && reverseRecord) { - const recordsWithoutEth = item.records.filter((record) => record.key !== 'eth') - const newRecords: ProfileRecord[] = [ - { key: 'eth', group: 'address', type: 'addr', value: address! }, - ...recordsWithoutEth, - ] - dispatch({ name: 'setProfileData', payload: { records: newRecords }, selected }) - } - - dispatch({ name: 'increaseStep', selected }) - } - - const profileCallback = ({ - records, - resolverAddress, - back, - }: RegistrationStepData['profile'] & BackObj) => { - dispatch({ name: 'setProfileData', payload: { records, resolverAddress }, selected }) - dispatch({ name: back ? 'decreaseStep' : 'increaseStep', selected }) - } - - const genericCallback = ({ back }: BackObj) => { - dispatch({ name: back ? 'decreaseStep' : 'increaseStep', selected }) - } - - const transactionsCallback = useCallback( - ({ back, resetSecret }: BackObj & { resetSecret?: boolean }) => { - if (resetSecret) { - dispatch({ name: 'resetSecret', selected }) - } - genericCallback({ back }) - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [selected], - ) - - const infoProfileCallback = () => { - dispatch({ - name: 'setQueue', - payload: ['pricing', 'profile', 'info', 'transactions', 'complete'], - selected, - }) - } - - const onStart = () => { - dispatch({ name: 'setStarted', selected }) - } - - const onComplete = (toProfile: boolean) => { - router.push(toProfile ? `/profile/${normalisedName}` : '/') - } + } = useMoonpayRegistration(normalisedName) + + // const pricingCallback = ({ + // seconds, + // reverseRecord, + // paymentMethodChoice, + // durationType, + // }: RegistrationStepData['pricing']) => { + // if (paymentMethodChoice === PaymentMethod.moonpay) { + // initiateMoonpayRegistrationMutation.mutate(secondsToYears(seconds)) + // return + // } + // dispatch({ + // name: 'setPricingData', + // payload: { seconds, reverseRecord, durationType }, + // selected, + // }) + // if (!item.queue.includes('profile')) { + // // if profile is not in queue, set the default profile data + // dispatch({ + // name: 'setProfileData', + // payload: { + // records: [{ key: 'eth', group: 'address', type: 'addr', value: address! }], + // clearRecords: resolverExists, + // resolverAddress: defaultResolverAddress, + // }, + // selected, + // }) + // if (reverseRecord) { + // // if reverse record is selected, add the profile step to the queue + // dispatch({ + // name: 'setQueue', + // payload: ['pricing', 'profile', 'info', 'transactions', 'complete'], + // selected, + // }) + // } + // } + + // // If profile is in queue and reverse record is selected, make sure that eth record is included and is set to address + // if (item.queue.includes('profile') && reverseRecord) { + // const recordsWithoutEth = item.records.filter((record) => record.key !== 'eth') + // const newRecords: ProfileRecord[] = [ + // { key: 'eth', group: 'address', type: 'addr', value: address! }, + // ...recordsWithoutEth, + // ] + // dispatch({ name: 'setProfileData', payload: { records: newRecords }, selected }) + // } + + // dispatch({ name: 'increaseStep', selected }) + // } + + // const profileCallback = ({ + // records, + // resolverAddress, + // back, + // }: RegistrationStepData['profile'] & BackObj) => { + // dispatch({ name: 'setProfileData', payload: { records, resolverAddress }, selected }) + // dispatch({ name: back ? 'decreaseStep' : 'increaseStep', selected }) + // } + + // const genericCallback = ({ back }: BackObj) => { + // dispatch({ name: back ? 'decreaseStep' : 'increaseStep', selected }) + // } + + // const transactionsCallback = useCallback( + // ({ back, resetSecret }: BackObj & { resetSecret?: boolean }) => { + // if (resetSecret) { + // dispatch({ name: 'resetSecret', selected }) + // } + // genericCallback({ back }) + // }, + // // eslint-disable-next-line react-hooks/exhaustive-deps + // [selected], + // ) + + // const infoProfileCallback = () => { + // dispatch({ + // name: 'setQueue', + // payload: ['pricing', 'profile', 'info', 'transactions', 'complete'], + // selected, + // }) + // } + + // const onStart = () => { + // dispatch({ name: 'setStarted', selected }) + // } + + // const onComplete = (toProfile: boolean) => { + // router.push(toProfile ? `/profile/${normalisedName}` : '/') + // } useEffect(() => { const handleRouteChange = (e: string) => { - if (e !== router.asPath && step === 'complete') { - dispatch({ name: 'clearItem', selected }) - cleanupFlow(commitKey) - cleanupFlow(registerKey) + if (e !== router.asPath && currentRegistrationFlowStep === 'complete') { + clearRegistrationFlow(normalisedName) } } router.events.on('routeChangeComplete', handleRouteChange) return () => { router.events.off('routeChangeComplete', handleRouteChange) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dispatch, step, selected, router.asPath]) + }, [ + currentRegistrationFlowStep, + clearRegistrationFlow, + router.asPath, + router.events, + normalisedName, + ]) const onDismissMoonpayModal = () => { if (moonpayTransactionStatus === 'waitingAuthorization') { @@ -257,7 +251,7 @@ const Registration = ({ nameDetails, isLoading }: Props) => { { ), - trailing: match([labelTooLong, step]) + trailing: match([labelTooLong, currentRegistrationFlowStep]) .with([true, P._], () => {t('error.nameTooLong')}) .with([false, 'pricing'], () => ( { beautifiedName={beautifiedName} gracePeriodEndDate={nameDetails.gracePeriodEndDate} resolverExists={resolverExists} - callback={pricingCallback} isPrimaryLoading={primary.isLoading} hasPrimaryName={!!primary.data?.name} - registrationData={item} moonpayTransactionStatus={moonpayTransactionStatus} initiateMoonpayRegistrationMutation={initiateMoonpayRegistrationMutation} /> )) .with([false, 'profile'], () => ( - - )) - .with([false, 'info'], () => ( - - )) - .with([false, 'transactions'], () => ( - + )) + .with([false, 'info'], () => ) + .with([false, 'transactions'], () => ) .with([false, 'complete'], () => ( - + )) .exhaustive(), }} @@ -336,11 +304,6 @@ const Registration = ({ nameDetails, isLoading }: Props) => { {t('steps.info.moonpayModalHeader')} - {chainId === 5 && ( - - {`${t('steps.info.moonpayTestCard')}: 4000 0209 5159 5032, 12/2030, 123`} - - )} css` @@ -151,19 +152,18 @@ const useEthInvoice = ( isMoonpayFlow: boolean, ): { InvoiceFilled?: React.ReactNode; avatarSrc?: string } => { const { t } = useTranslation('register') - const { address } = useAccount() - const keySuffix = `${name}-${address}` - const commitKey = `commit-${keySuffix}` - const registerKey = `register-${keySuffix}` - const { getLatestTransaction } = useTransactionFlow() - const commitTxFlow = getLatestTransaction(commitKey) - const registerTxFlow = getLatestTransaction(registerKey) + const commitTransaction = useTransactionManager((s) => s.getCurrentCommitTransaction(name)) + const registerTransaction = useTransactionManager((s) => s.getCurrentRegisterTransaction(name)) const [avatarSrc, setAvatarSrc] = useState() - const commitReceipt = commitTxFlow?.minedData - const registerReceipt = registerTxFlow?.minedData + const { data: commitReceipt } = useTransactionReceipt({ + hash: commitTransaction?.currentHash ?? undefined, + }) + const { data: registerReceipt } = useTransactionReceipt({ + hash: registerTransaction?.currentHash ?? undefined, + }) const registrationValue = useMemo(() => { if (!registerReceipt) return null @@ -227,14 +227,23 @@ const useEthInvoice = ( type Props = { name: string beautifiedName: string - callback: (toProfile: boolean) => void - isMoonpayFlow: boolean } -const Complete = ({ name, beautifiedName, callback, isMoonpayFlow }: Props) => { +const Complete = ({ name, beautifiedName }: Props) => { const { t } = useTranslation('register') const { width, height } = useWindowSize() - const { InvoiceFilled, avatarSrc } = useEthInvoice(name, isMoonpayFlow) + + const router = useRouterWithHistory() + + const paymentMethod = useTransactionManager( + (s) => s.getCurrentRegistrationFlowOrDefault(name).paymentMethodChoice, + ) + + const onComplete = (toProfile: boolean) => { + router.push(toProfile ? `/profile/${name}` : '/') + } + + const { InvoiceFilled, avatarSrc } = useEthInvoice(name, paymentMethod === 'moonpay') const nameWithColourEmojis = useMemo(() => { const data = tokenise(beautifiedName) @@ -289,12 +298,12 @@ const Complete = ({ name, beautifiedName, callback, isMoonpayFlow }: Props) => { {InvoiceFilled} - - diff --git a/src/components/pages/profile/[name]/registration/steps/Info.tsx b/src/components/pages/register/steps/Info.tsx similarity index 74% rename from src/components/pages/profile/[name]/registration/steps/Info.tsx rename to src/components/pages/register/steps/Info.tsx index b6dc35a0d..0e77630e0 100644 --- a/src/components/pages/profile/[name]/registration/steps/Info.tsx +++ b/src/components/pages/register/steps/Info.tsx @@ -6,9 +6,9 @@ import { Button, Heading, mq, Typography } from '@ensdomains/thorin' import MobileFullWidth from '@app/components/@atoms/MobileFullWidth' import { Card } from '@app/components/Card' import { useEstimateFullRegistration } from '@app/hooks/gasEstimation/useEstimateRegistration' +import { useTransactionManager } from '@app/transaction/transactionManager' import FullInvoice from '../FullInvoice' -import { RegistrationReducerDataItem } from '../types' const StyledCard = styled(Card)( ({ theme }) => css` @@ -97,18 +97,23 @@ const ProfileButton = styled.button( const infoItemArr = Array.from({ length: 3 }, (_, i) => `steps.info.ethItems.${i}`) type Props = { - registrationData: RegistrationReducerDataItem name: string - callback: (data: { back: boolean }) => void - onProfileClick: () => void } -const Info = ({ registrationData, name, callback, onProfileClick }: Props) => { +const Info = ({ name }: Props) => { const { t } = useTranslation('register') + const existingRegistrationData = useTransactionManager((s) => + s.getCurrentRegistrationFlowOrDefault(name), + ) + const setRegistrationFlowQueue = useTransactionManager((s) => s.setRegistrationFlowQueue) + const onRegistrationInfoStepCompleted = useTransactionManager( + (s) => s.onRegistrationInfoStepCompleted, + ) + const estimate = useEstimateFullRegistration({ name, - registrationData, + registrationData: existingRegistrationData, }) return ( @@ -124,8 +129,19 @@ const Info = ({ registrationData, name, callback, onProfileClick }: Props) => { ))} - {!registrationData.queue.includes('profile') && ( - + {!existingRegistrationData.queue.includes('profile') && ( + + setRegistrationFlowQueue(name, [ + 'pricing', + 'profile', + 'info', + 'transactions', + 'complete', + ]) + } + > {t('steps.info.setupProfile')} @@ -133,12 +149,18 @@ const Info = ({ registrationData, name, callback, onProfileClick }: Props) => { )} - - diff --git a/src/components/pages/register/steps/Pricing/PaymentChoice.tsx b/src/components/pages/register/steps/Pricing/PaymentChoice.tsx new file mode 100644 index 000000000..0505f727d --- /dev/null +++ b/src/components/pages/register/steps/Pricing/PaymentChoice.tsx @@ -0,0 +1,315 @@ +import { Dispatch, SetStateAction, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' + +import { + Field, + Helper, + mq, + RadioButton, + RadioButtonGroup, + Toggle, + Typography, +} from '@ensdomains/thorin' + +import MoonpayLogo from '@app/assets/MoonpayLogo.svg' +import { Spacer } from '@app/components/@atoms/Spacer' +import type { RegistrationPaymentMethod } from '@app/transaction/slices/createRegistrationFlowSlice' +import { useBreakpoint } from '@app/utils/BreakpointProvider' + +import { type MoonpayTransactionStatus } from '../../types' + +const OutlinedContainer = styled.div( + ({ theme }) => css` + width: ${theme.space.full}; + display: grid; + align-items: center; + grid-template-areas: 'title checkbox' 'description description'; + gap: ${theme.space['2']}; + + padding: ${theme.space['4']}; + border-radius: ${theme.radii.large}; + background: ${theme.colors.backgroundSecondary}; + + ${mq.sm.min(css` + grid-template-areas: 'title checkbox' 'description checkbox'; + `)} + `, +) + +const gridAreaStyle = ({ $name }: { $name: string }) => css` + grid-area: ${$name}; +` + +const moonpayInfoItems = Array.from({ length: 2 }, (_, i) => `steps.info.moonpayItems.${i}`) + +const PaymentChoiceContainer = styled.div` + width: 100%; +` + +const StyledRadioButtonGroup = styled(RadioButtonGroup)( + ({ theme }) => css` + border: 1px solid ${theme.colors.border}; + border-radius: ${theme.radii.large}; + gap: 0; + `, +) + +const StyledRadioButton = styled(RadioButton)`` + +const RadioButtonContainer = styled.div( + ({ theme }) => css` + padding: ${theme.space['4']}; + &:last-child { + border-top: 1px solid ${theme.colors.border}; + } + `, +) + +const StyledTitle = styled(Typography)` + margin-left: 15px; +` + +const RadioLabel = styled(Typography)( + ({ theme }) => css` + margin-right: 10px; + color: ${theme.colors.text}; + `, +) + +const MoonpayContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + gap: 5px; +` + +const InfoItems = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: ${theme.space['4']}; + + ${mq.sm.min(css` + flex-direction: row; + align-items: stretch; + `)} + `, +) + +const InfoItem = styled.div( + ({ theme }) => css` + width: 100%; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: ${theme.space['4']}; + + padding: ${theme.space['4']}; + border: 1px solid ${theme.colors.border}; + border-radius: ${theme.radii.large}; + text-align: center; + + & > div:first-of-type { + width: ${theme.space['10']}; + height: ${theme.space['10']}; + display: flex; + align-items: center; + justify-content: center; + font-size: ${theme.fontSizes.extraLarge}; + font-weight: ${theme.fontWeights.bold}; + color: ${theme.colors.backgroundPrimary}; + background: ${theme.colors.accentPrimary}; + border-radius: ${theme.radii.full}; + } + + & > div:last-of-type { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + } + `, +) + +const LabelContainer = styled.div` + display: flex; + flex-wrap: wrap; +` + +const CheckboxWrapper = styled.div( + () => css` + width: 100%; + `, + gridAreaStyle, +) + +const OutlinedContainerDescription = styled(Typography)(gridAreaStyle) + +const OutlinedContainerTitle = styled(Typography)( + ({ theme }) => css` + font-size: ${theme.fontSizes.large}; + font-weight: ${theme.fontWeights.bold}; + white-space: nowrap; + `, + gridAreaStyle, +) + +const EthInnerCheckbox = ({ + address, + hasPrimaryName, + reverseRecord, + setReverseRecord, + started, +}: { + address: string + hasPrimaryName: boolean + reverseRecord: boolean + setReverseRecord: (val: boolean) => void + started: boolean +}) => { + const { t } = useTranslation('register') + const breakpoints = useBreakpoint() + + useEffect(() => { + if (!started) { + setReverseRecord(!hasPrimaryName) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setReverseRecord]) + + return ( + + + {(ids) => ( + { + e.stopPropagation() + setReverseRecord(e.target.checked) + }} + data-testid="primary-name-toggle" + /> + )} + + + ) +} + +export const PaymentChoice = ({ + paymentMethodChoice, + setPaymentMethodChoice, + hasEnoughEth, + moonpayTransactionStatus, + address, + hasPrimaryName, + reverseRecord, + setReverseRecord, + started, +}: { + paymentMethodChoice: RegistrationPaymentMethod + setPaymentMethodChoice: Dispatch> + hasEnoughEth: boolean + moonpayTransactionStatus: MoonpayTransactionStatus | undefined + address: string + hasPrimaryName: boolean + reverseRecord: boolean + setReverseRecord: (reverseRecord: boolean) => void + started: boolean +}) => { + const { t } = useTranslation('register') + + return ( + + + {t('steps.info.paymentMethod')} + + + setPaymentMethodChoice(e.target.value as RegistrationPaymentMethod)} + > + + {t('steps.info.ethereum')}} + name="RadioButtonGroup" + value="ethereum" + disabled={moonpayTransactionStatus === 'pending'} + checked={paymentMethodChoice === 'ethereum' || undefined} + /> + {paymentMethodChoice === 'ethereum' && !hasEnoughEth && ( + <> + + + {t('steps.info.notEnoughEth')} + + + + )} + {paymentMethodChoice === 'ethereum' && hasEnoughEth && ( + <> + + + + {t('steps.pricing.primaryName')} + + + + {t('steps.pricing.primaryNameMessage')} + + + + + )} + + + + {t('steps.info.creditOrDebit')} + + ({t('steps.info.additionalFee')}) + + + } + name="RadioButtonGroup" + value="moonpay" + checked={paymentMethodChoice === 'moonpay' || undefined} + /> + {paymentMethodChoice === 'moonpay' && ( + <> + + + {moonpayInfoItems.map((item, idx) => ( + + {idx + 1} + {t(item)} + + ))} + + + {moonpayTransactionStatus === 'failed' && ( + {t('steps.info.failedMoonpayTransaction')} + )} + + + {t('steps.info.poweredBy')} + + + + )} + + + + ) +} diff --git a/src/components/pages/profile/[name]/registration/steps/Pricing/Pricing.test.tsx b/src/components/pages/register/steps/Pricing/Pricing.test.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Pricing/Pricing.test.tsx rename to src/components/pages/register/steps/Pricing/Pricing.test.tsx diff --git a/src/components/pages/register/steps/Pricing/Pricing.tsx b/src/components/pages/register/steps/Pricing/Pricing.tsx new file mode 100644 index 000000000..65eb8a1ab --- /dev/null +++ b/src/components/pages/register/steps/Pricing/Pricing.tsx @@ -0,0 +1,313 @@ +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { usePreviousDistinct } from 'react-use' +import usePrevious from 'react-use/lib/usePrevious' +import styled, { css } from 'styled-components' +import { match, P } from 'ts-pattern' +import type { Address } from 'viem' +import { useBalance } from 'wagmi' +import { GetBalanceData } from 'wagmi/query' +import { useShallow } from 'zustand/react/shallow' + +import { Button, Heading, mq } from '@ensdomains/thorin' + +import MobileFullWidth from '@app/components/@atoms/MobileFullWidth' +import { RegistrationTimeComparisonBanner } from '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner' +import { DateSelection } from '@app/components/@molecules/DateSelection/DateSelection' +import { Card } from '@app/components/Card' +import { ConnectButton } from '@app/components/ConnectButton' +import { useAccountSafely } from '@app/hooks/account/useAccountSafely' +import { useContractAddress } from '@app/hooks/chain/useContractAddress' +import { useEstimateFullRegistration } from '@app/hooks/gasEstimation/useEstimateRegistration' +import type { + RegistrationDurationType, + RegistrationPaymentMethod, +} from '@app/transaction/slices/createRegistrationFlowSlice' +import { useTransactionManager } from '@app/transaction/transactionManager' +import { ONE_DAY, ONE_YEAR } from '@app/utils/time' + +import FullInvoice from '../../FullInvoice' +import { MoonpayTransactionStatus, PaymentMethod } from '../../types' +import { + useMoonpayRegistration, + type InitiateMoonpayRegistrationMutationResult, +} from '../../useMoonpayRegistration' +import { PaymentChoice } from './PaymentChoice' +import TemporaryPremium from './TemporaryPremium' + +const StyledCard = styled(Card)( + ({ theme }) => css` + max-width: 780px; + margin: 0 auto; + flex-direction: column; + gap: ${theme.space['4']}; + padding: ${theme.space['4']}; + + ${mq.sm.min(css` + padding: ${theme.space['6']} ${theme.space['18']}; + gap: ${theme.space['6']}; + `)} + `, +) + +const StyledHeading = styled(Heading)( + () => css` + width: 100%; + word-break: break-all; + + @supports (overflow-wrap: anywhere) { + overflow-wrap: anywhere; + word-break: normal; + } + `, +) + +export type ActionButtonProps = { + address?: Address + moonpayTransactionStatus?: MoonpayTransactionStatus + callback: () => void + paymentMethodChoice: RegistrationPaymentMethod + initiateMoonpayRegistrationMutation: InitiateMoonpayRegistrationMutationResult + balance: GetBalanceData | undefined + totalRequiredBalance?: bigint +} + +export const ActionButton = (props: ActionButtonProps) => { + const { t } = useTranslation('register') + + return match(props) + .with({ address: P.nullish }, () => ) + .with({ moonpayTransactionStatus: 'pending' }, () => ( + + )) + .with({ moonpayTransactionStatus: 'failed', paymentMethodChoice: 'moonpay' }, () => ( + + )) + .with( + { paymentMethodChoice: 'moonpay' }, + ({ initiateMoonpayRegistrationMutation, callback, paymentMethodChoice }) => ( + + ), + ) + .with( + P.when((_props) => typeof _props.balance?.value !== 'bigint' || !_props.totalRequiredBalance), + () => ( + + ), + ) + .with( + P.when( + (_props) => + _props.totalRequiredBalance && + typeof _props.balance?.value === 'bigint' && + _props.balance.value < _props.totalRequiredBalance && + _props.paymentMethodChoice === PaymentMethod.ethereum, + ), + () => ( + + ), + ) + .otherwise(({ callback, paymentMethodChoice }) => ( + + )) +} + +export type PricingProps = { + name: string + gracePeriodEndDate: Date | undefined + beautifiedName: string + + resolverExists: boolean | undefined + isPrimaryLoading: boolean + hasPrimaryName: boolean + moonpayTransactionStatus?: MoonpayTransactionStatus + initiateMoonpayRegistrationMutation: ReturnType< + typeof useMoonpayRegistration + >['initiateMoonpayRegistrationMutation'] +} + +const minSeconds = 28 * ONE_DAY + +const Pricing = ({ + name, + gracePeriodEndDate, + beautifiedName, + isPrimaryLoading, + hasPrimaryName, + resolverExists, + moonpayTransactionStatus, + initiateMoonpayRegistrationMutation, +}: PricingProps) => { + const { t } = useTranslation('register') + + const { address } = useAccountSafely() + const { data: balance } = useBalance({ address }) + const resolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + + const existingRegistrationData = useTransactionManager( + useShallow((s) => s.getCurrentRegistrationFlowOrDefault(name)), + ) + const onRegistrationPricingStepCompleted = useTransactionManager( + (s) => s.onRegistrationPricingStepCompleted, + ) + + const [seconds, setSeconds] = useState(() => existingRegistrationData.seconds ?? ONE_YEAR) + const [durationType, setDurationType] = useState( + existingRegistrationData.durationType ?? 'years', + ) + + const [reverseRecord, setReverseRecord] = useState(() => + existingRegistrationData.isStarted ? existingRegistrationData.reverseRecord : !hasPrimaryName, + ) + + const hasPendingMoonpayTransaction = moonpayTransactionStatus === 'pending' + const hasFailedMoonpayTransaction = moonpayTransactionStatus === 'failed' + + const previousMoonpayTransactionStatus = usePrevious(moonpayTransactionStatus) + + const [paymentMethodChoice, setPaymentMethodChoice] = useState( + hasPendingMoonpayTransaction ? 'moonpay' : 'ethereum', + ) + + const callback = useCallback(() => { + onRegistrationPricingStepCompleted(name, { + durationType, + initiateMoonpayRegistrationMutation, + paymentMethodChoice, + resolverExists: !!resolverExists, + reverseRecord, + seconds, + }) + }, [ + onRegistrationPricingStepCompleted, + name, + durationType, + initiateMoonpayRegistrationMutation, + paymentMethodChoice, + resolverExists, + reverseRecord, + seconds, + ]) + + // Keep radio button choice up to date + useEffect(() => { + if (moonpayTransactionStatus) { + setPaymentMethodChoice( + hasPendingMoonpayTransaction || hasFailedMoonpayTransaction + ? PaymentMethod.moonpay + : PaymentMethod.ethereum, + ) + } + }, [ + hasFailedMoonpayTransaction, + hasPendingMoonpayTransaction, + moonpayTransactionStatus, + previousMoonpayTransactionStatus, + setPaymentMethodChoice, + ]) + + const fullEstimate = useEstimateFullRegistration({ + name, + registrationData: { + ...existingRegistrationData, + reverseRecord, + seconds, + records: [{ key: 'ETH', value: resolverAddress, type: 'addr', group: 'address' }], + clearRecords: resolverExists ?? false, + resolverAddress, + }, + }) + + const { hasPremium, premiumFee, gasPrice, yearlyFee, totalDurationBasedFee, estimatedGasFee } = + fullEstimate + const durationRequiredBalance = totalDurationBasedFee ? (totalDurationBasedFee * 110n) / 100n : 0n + const totalRequiredBalance = durationRequiredBalance + ? durationRequiredBalance + (premiumFee || 0n) + (estimatedGasFee || 0n) + : 0n + + const previousYearlyFee = usePreviousDistinct(yearlyFee) || 0n + + const unsafeDisplayYearlyFee = yearlyFee === 0n ? previousYearlyFee : yearlyFee + + const showPaymentChoice = !isPrimaryLoading && address + + const previousEstimatedGasFee = usePreviousDistinct(estimatedGasFee) || 0n + + const unsafeDisplayEstimatedGasFee = + estimatedGasFee === 0n ? previousEstimatedGasFee : estimatedGasFee + + return ( + + {t('heading', { name: beautifiedName })} + + + {hasPremium && gracePeriodEndDate ? ( + + ) : ( + !!unsafeDisplayYearlyFee && + !!unsafeDisplayEstimatedGasFee && + !!gasPrice && ( + + ) + )} + {showPaymentChoice && ( + + )} + + + + + ) +} + +export default Pricing diff --git a/src/components/pages/profile/[name]/registration/steps/Pricing/TemporaryPremium.tsx b/src/components/pages/register/steps/Pricing/TemporaryPremium.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Pricing/TemporaryPremium.tsx rename to src/components/pages/register/steps/Pricing/TemporaryPremium.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView.test.tsx b/src/components/pages/register/steps/Profile/AddProfileRecordView.test.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView.test.tsx rename to src/components/pages/register/steps/Profile/AddProfileRecordView.test.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView.tsx b/src/components/pages/register/steps/Profile/AddProfileRecordView.tsx similarity index 99% rename from src/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView.tsx rename to src/components/pages/register/steps/Profile/AddProfileRecordView.tsx index 08b89c3fe..ae851e4fd 100644 --- a/src/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView.tsx +++ b/src/components/pages/register/steps/Profile/AddProfileRecordView.tsx @@ -14,9 +14,9 @@ import { ProfileRecord, ProfileRecordGroup, } from '@app/constants/profileRecordOptions' +import useDebouncedCallback from '@app/hooks/useDebouncedCallback' import { ProfileEditorForm } from '@app/hooks/useProfileEditorForm' -import useDebouncedCallback from '../../../../../../../hooks/useDebouncedCallback' import { OptionButton } from './OptionButton' import { OptionGroup } from './OptionGroup' diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput.tsx b/src/components/pages/register/steps/Profile/CustomProfileRecordInput.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput.tsx rename to src/components/pages/register/steps/Profile/CustomProfileRecordInput.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/DynamicIcon.tsx b/src/components/pages/register/steps/Profile/DynamicIcon.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/DynamicIcon.tsx rename to src/components/pages/register/steps/Profile/DynamicIcon.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/Field.tsx b/src/components/pages/register/steps/Profile/Field.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/Field.tsx rename to src/components/pages/register/steps/Profile/Field.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/OptionButton.tsx b/src/components/pages/register/steps/Profile/OptionButton.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/OptionButton.tsx rename to src/components/pages/register/steps/Profile/OptionButton.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/OptionGroup.tsx b/src/components/pages/register/steps/Profile/OptionGroup.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/OptionGroup.tsx rename to src/components/pages/register/steps/Profile/OptionGroup.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/Profile.test.tsx b/src/components/pages/register/steps/Profile/Profile.test.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/Profile.test.tsx rename to src/components/pages/register/steps/Profile/Profile.test.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/Profile.tsx b/src/components/pages/register/steps/Profile/Profile.tsx similarity index 93% rename from src/components/pages/profile/[name]/registration/steps/Profile/Profile.tsx rename to src/components/pages/register/steps/Profile/Profile.tsx index bbabfbad7..dd0dbc60f 100644 --- a/src/components/pages/profile/[name]/registration/steps/Profile/Profile.tsx +++ b/src/components/pages/register/steps/Profile/Profile.tsx @@ -15,8 +15,8 @@ import { ProfileRecord } from '@app/constants/profileRecordOptions' import { useContractAddress } from '@app/hooks/chain/useContractAddress' import { useLocalStorage } from '@app/hooks/useLocalStorage' import { ProfileEditorForm, useProfileEditorForm } from '@app/hooks/useProfileEditorForm' +import { useTransactionManager } from '@app/transaction/transactionManager' -import { BackObj, RegistrationReducerDataItem, RegistrationStepData } from '../../types' import { AddProfileRecordView } from './AddProfileRecordView' import { CustomProfileRecordInput } from './CustomProfileRecordInput' import { ProfileRecordInput } from './ProfileRecordInput' @@ -110,17 +110,22 @@ type ModalOption = AvatarClickType | 'add-record' | 'clear-eth' | 'public-notice type Props = { name: string - registrationData: RegistrationReducerDataItem resolverExists: boolean | undefined - callback: (data: RegistrationStepData['profile'] & BackObj) => void } -const Profile = ({ name, callback, registrationData, resolverExists }: Props) => { +const Profile = ({ name, resolverExists }: Props) => { const { t } = useTranslation('register') + const existingRegistrationData = useTransactionManager((s) => + s.getCurrentRegistrationFlowOrDefault(name), + ) + const onRegistrationProfileStepCompleted = useTransactionManager( + (s) => s.onRegistrationProfileStepCompleted, + ) + const defaultResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) const clearRecords = - registrationData.resolverAddress === defaultResolverAddress ? resolverExists : false + existingRegistrationData.resolverAddress === defaultResolverAddress ? resolverExists : false const backRef = useRef(null) const { @@ -140,7 +145,7 @@ const Profile = ({ name, callback, registrationData, resolverExists }: Props) => errorForRecordAtIndex, isDirtyForRecordAtIndex, hasErrors, - } = useProfileEditorForm(registrationData.records) + } = useProfileEditorForm(existingRegistrationData.records) const [isAvatarDropdownOpen, setIsAvatarDropdownOpen] = useState(false) @@ -185,10 +190,10 @@ const Profile = ({ name, callback, registrationData, resolverExists }: Props) => const nativeEvent = e?.nativeEvent as SubmitEvent | undefined const newRecords = profileEditorFormToProfileRecords(data) - callback({ + onRegistrationProfileStepCompleted(name, { records: newRecords, clearRecords, - resolverAddress: registrationData.resolverAddress, + resolverAddress: existingRegistrationData.resolverAddress, back: nativeEvent?.submitter === backRef.current, }) } @@ -302,7 +307,7 @@ const Profile = ({ name, callback, registrationData, resolverExists }: Props) => key={field.id} recordKey={field.key} group={field.group} - disabled={field.key === 'eth' && registrationData.reverseRecord} + disabled={field.key === 'eth' && existingRegistrationData.reverseRecord} label={labelForRecord(field)} secondaryLabel={secondaryLabelForRecord(field)} placeholder={placeholderForRecord(field)} diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput.tsx b/src/components/pages/register/steps/Profile/ProfileRecordInput.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput.tsx rename to src/components/pages/register/steps/Profile/ProfileRecordInput.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea.tsx b/src/components/pages/register/steps/Profile/ProfileRecordTextarea.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea.tsx rename to src/components/pages/register/steps/Profile/ProfileRecordTextarea.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/WrappedAvatarButton.tsx b/src/components/pages/register/steps/Profile/WrappedAvatarButton.tsx similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/WrappedAvatarButton.tsx rename to src/components/pages/register/steps/Profile/WrappedAvatarButton.tsx diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils.test.ts b/src/components/pages/register/steps/Profile/profileRecordUtils.test.ts similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils.test.ts rename to src/components/pages/register/steps/Profile/profileRecordUtils.test.ts diff --git a/src/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils.ts b/src/components/pages/register/steps/Profile/profileRecordUtils.ts similarity index 100% rename from src/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils.ts rename to src/components/pages/register/steps/Profile/profileRecordUtils.ts diff --git a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx b/src/components/pages/register/steps/Transactions.tsx similarity index 60% rename from src/components/pages/profile/[name]/registration/steps/Transactions.tsx rename to src/components/pages/register/steps/Transactions.tsx index 6fa915441..700e4e6ec 100644 --- a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx +++ b/src/components/pages/register/steps/Transactions.tsx @@ -1,25 +1,18 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { match, P } from 'ts-pattern' -import { useAccount } from 'wagmi' -import { makeCommitment } from '@ensdomains/ensjs/utils' import { Button, CountdownCircle, Dialog, Heading, mq, Spinner } from '@ensdomains/thorin' import MobileFullWidth from '@app/components/@atoms/MobileFullWidth' import { TextWithTooltip } from '@app/components/@atoms/TextWithTooltip/TextWithTooltip' import { Card } from '@app/components/Card' -import { useExistingCommitment } from '@app/hooks/registration/useExistingCommitment' import { useDurationCountdown } from '@app/hooks/time/useDurationCountdown' -import useRegistrationParams from '@app/hooks/useRegistrationParams' -import { CenteredTypography } from '@app/transaction-flow/input/ProfileEditor/components/CenteredTypography' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { useTransactionManager } from '@app/transaction/transactionManager' +import { CenteredTypography } from '@app/transaction/user/input/ProfileEditor/components/CenteredTypography' import { ONE_DAY } from '@app/utils/time' -import { RegistrationReducerDataItem } from '../types' - const StyledCard = styled(Card)( ({ theme }) => css` max-width: 780px; @@ -89,98 +82,83 @@ const ProgressButton = ({ onClick, label }: { onClick: () => void; label: string type Props = { name: string - registrationData: RegistrationReducerDataItem - callback: (data: { back: boolean; resetSecret?: boolean }) => void - onStart: () => void } -const Transactions = ({ registrationData, name, callback, onStart }: Props) => { +const Transactions = ({ name }: Props) => { const { t } = useTranslation('register') - const { address } = useAccount() - const keySuffix = `${name}-${address}` - const commitKey = `commit-${keySuffix}` - const registerKey = `register-${keySuffix}` - const { getLatestTransaction, createTransactionFlow, resumeTransactionFlow, cleanupFlow } = - useTransactionFlow() - const commitTx = getLatestTransaction(commitKey) - const registerTx = getLatestTransaction(registerKey) + const commitTransaction = useTransactionManager((s) => s.getCurrentCommitTransaction(name)) + const registerTransaction = useTransactionManager((s) => s.getCurrentRegisterTransaction(name)) + const [resetOpen, setResetOpen] = useState(false) - const commitTimestamp = commitTx?.stage === 'complete' ? commitTx?.finaliseTime : undefined + const commitTimestamp = + commitTransaction?.status === 'success' ? commitTransaction?.receipt.timestamp : undefined const [commitComplete, setCommitComplete] = useState( !!commitTimestamp && commitTimestamp + 60000 < Date.now(), ) - const registrationParams = useRegistrationParams({ - name, - owner: address!, - registrationData, - }) - - const commitCouldBeFound = - !commitTx?.stage || commitTx.stage === 'confirm' || commitTx.stage === 'failed' - useExistingCommitment({ - commitment: makeCommitment(registrationParams), - enabled: commitCouldBeFound, - commitKey, - }) - - const makeCommitNameFlow = useCallback(() => { - onStart() - createTransactionFlow(commitKey, { - transactions: [createTransactionItem('commitName', registrationParams)], - requiresManualCleanup: true, - autoClose: true, - resumeLink: `/register/${name}`, - }) - }, [commitKey, createTransactionFlow, name, onStart, registrationParams]) - - const makeRegisterNameFlow = () => { - createTransactionFlow(registerKey, { - transactions: [createTransactionItem('registerName', registrationParams)], - requiresManualCleanup: true, - autoClose: true, - resumeLink: `/register/${name}`, - }) - } - - const showCommitTransaction = () => { - resumeTransactionFlow(commitKey) - } - - const showRegisterTransaction = () => { - resumeTransactionFlow(registerKey) - } + const existingRegistrationData = useTransactionManager((s) => + s.getCurrentRegistrationFlowOrDefault(name), + ) + const resetRegistrationTransactions = useTransactionManager( + (s) => () => s.resetRegistrationTransactions(name), + ) + const startCommitNameTransaction = useTransactionManager( + (s) => () => s.startCommitNameTransaction(name), + ) + const startRegisterNameTransaction = useTransactionManager( + (s) => () => s.startRegisterNameTransaction(name), + ) + const onRegistrationTransactionsStepCompleted = useTransactionManager( + (s) => s.onRegistrationTransactionsStepCompleted, + ) + const resumeCommitNameTransaction = useTransactionManager( + (s) => () => s.resumeCommitNameTransaction(name), + ) + const resumeRegisterNameTransaction = useTransactionManager( + (s) => () => s.resumeRegisterNameTransaction(name), + ) - const resetTransactions = () => { - cleanupFlow(commitKey) - cleanupFlow(registerKey) - callback({ back: true, resetSecret: true }) - setResetOpen(false) - } + // const commitCouldBeFound = + // !commitTransaction?.status || commitTransaction.status === 'empty' || commitTransaction.status === 'waitingForUser' || commitTransaction.status === 'reverted' + // useExistingCommitment({ + // commitment: makeCommitment(registrationParams), + // enabled: commitCouldBeFound, + // commitKey, + // }) - useEffect(() => { - if (!commitTx) { - makeCommitNameFlow() - } - }, [commitTx, makeCommitNameFlow]) + // const makeCommitNameFlow = useCallback(() => { + // onStart() + // createTransactionFlow(commitKey, { + // transactions: [createTransactionItem('commitName', registrationParams)], + // requiresManualCleanup: true, + // autoClose: true, + // resumeLink: `/register/${name}`, + // }) + // }, [commitKey, createTransactionFlow, name, onStart, registrationParams]) - useEffect(() => { - if (registerTx?.stage === 'complete') { - callback({ back: false }) - } - }, [callback, registerTx?.stage]) + // const makeRegisterNameFlow = () => { + // createTransactionFlow(registerKey, { + // transactions: [createTransactionItem('registerName', registrationParams)], + // requiresManualCleanup: true, + // autoClose: true, + // resumeLink: `/register/${name}`, + // }) + // } const NormalBackButton = useMemo( () => ( - ), - [t, callback], + [t, name, onRegistrationTransactionsStepCompleted], ) const ResetBackButton = useMemo( @@ -198,6 +176,27 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { endDate: commitTimestamp ? new Date(commitTimestamp + ONE_DAY * 1000) : undefined, }) + const resetTransactions = () => { + resetRegistrationTransactions() + setResetOpen(false) + } + + if (!commitTransaction) { + startCommitNameTransaction() + } + + useEffect(() => { + if (!commitTransaction) { + startCommitNameTransaction() + } + }, [commitTransaction, startCommitNameTransaction]) + + useEffect(() => { + if (registerTransaction?.status === 'success') { + onRegistrationTransactionsStepCompleted(name, { back: false }) + } + }, [registerTransaction?.status, onRegistrationTransactionsStepCompleted, name]) + return ( setResetOpen(false)}> @@ -228,8 +227,8 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { callback={() => setCommitComplete(true)} /> - {match([commitTx, commitComplete, duration]) - .with([{ stage: 'complete' }, false, P._], () => ( + {match([commitTransaction, commitComplete, duration]) + .with([{ status: 'success' }, false, P._], () => ( { }} /> )) - .with([{ stage: 'complete' }, true, null], () => + .with([{ status: 'success' }, true, null], () => t('steps.transactions.subheading.commitExpired'), ) - .with([{ stage: 'complete' }, true, P.not(P.nullish)], ([, , d]) => + .with([{ status: 'success' }, true, P.not(P.nullish)], ([, , d]) => t('steps.transactions.subheading.commitComplete', { duration: d }), ) - .with([{ stage: 'complete' }, true, P._], () => + .with([{ status: 'success' }, true, P._], () => t('steps.transactions.subheading.commitCompleteNoDuration'), ) .otherwise(() => t('steps.transactions.subheading.default'))} - {match([commitComplete, registerTx, commitTx]) - .with([true, { stage: 'failed' }, P._], () => ( + {match([commitComplete, registerTransaction, commitTransaction]) + .with([true, { status: 'reverted' }, P._], () => ( <> {ResetBackButton} )) - .with([true, { stage: 'sent' }, P._], () => ( + .with([true, { status: 'pending' }, P._], () => ( )) .with([true, P._, P._], () => ( @@ -276,29 +275,33 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { )) - .with([false, P._, { stage: 'failed' }], () => ( + .with([false, P._, { status: 'reverted' }], () => ( <> {NormalBackButton} )) - .with([false, P._, { stage: 'sent' }], () => ( + .with([false, P._, { status: 'pending' }], () => ( )) - .with([false, P._, { stage: 'complete' }], () => ( + .with([false, P._, { status: 'success' }], () => ( <> {ResetBackButton} @@ -311,12 +314,15 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { .otherwise(() => ( <> - - diff --git a/src/components/pages/profile/[name]/registration/types.ts b/src/components/pages/register/types.ts similarity index 100% rename from src/components/pages/profile/[name]/registration/types.ts rename to src/components/pages/register/types.ts diff --git a/src/components/pages/profile/[name]/registration/useMoonpayRegistration.test.ts b/src/components/pages/register/useMoonpayRegistration.test.ts similarity index 100% rename from src/components/pages/profile/[name]/registration/useMoonpayRegistration.test.ts rename to src/components/pages/register/useMoonpayRegistration.test.ts diff --git a/src/components/pages/register/useMoonpayRegistration.ts b/src/components/pages/register/useMoonpayRegistration.ts new file mode 100644 index 000000000..7165b5292 --- /dev/null +++ b/src/components/pages/register/useMoonpayRegistration.ts @@ -0,0 +1,149 @@ +import { + useMutation, + type QueryFunctionContext, + type UseMutationResult, +} from '@tanstack/react-query' +import { useState } from 'react' +import { labelhash, type Address } from 'viem' + +import type { SupportedChain } from '@app/constants/chains' +import { useQueryOptions } from '@app/hooks/useQueryOptions' +import { useTransactionManager } from '@app/transaction/transactionManager' +import type { ConfigWithEns, CreateQueryKey } from '@app/types' +import { MOONPAY_WORKER_URL } from '@app/utils/constants' +import { useQuery } from '@app/utils/query/useQuery' +import { getLabelFromName } from '@app/utils/utils' + +import { MoonpayTransactionStatus } from './types' + +type MoonpayRegistrationMutationParameters = { + duration: number + name: string + chainId: SupportedChain['id'] + address: Address +} + +type MoonpayRegistrationMutationReturnType = { + externalTransactionId: string + moonpayUrl: string +} + +const initiateMoonpayRegistrationMutationFn = async ({ + name, + duration, + chainId, + address, +}: MoonpayRegistrationMutationParameters): Promise => { + const label = getLabelFromName(name) + const tokenId = labelhash(label) + + const requestUrl = `${ + MOONPAY_WORKER_URL[chainId] + }/signedurl?tokenId=${tokenId}&name=${encodeURIComponent( + name, + )}&duration=${duration}&walletAddress=${address}` + + const response = await fetch(requestUrl) + const moonpayUrl = await response.text() + + const params = new URLSearchParams(moonpayUrl) + const externalTransactionId = params.get('externalTransactionId') + + if (!externalTransactionId) throw new Error('No external transaction id found') + + return { externalTransactionId, moonpayUrl } +} + +export type InitiateMoonpayRegistrationMutationResult = UseMutationResult< + MoonpayRegistrationMutationReturnType, + Error, + MoonpayRegistrationMutationParameters, + unknown +> + +type GetMoonpayStatusQueryParameters = { + name: string + externalTransactionId: string +} +type GetMoonpayStatusQueryKey = CreateQueryKey< + GetMoonpayStatusQueryParameters, + 'getMoonpayStatus', + 'standard' +> + +const getMoonpayStatusQueryFn = + (_config: ConfigWithEns) => + async ({ + queryKey: [{ name, externalTransactionId }, chainId, address], + }: QueryFunctionContext) => { + if (!address) throw new Error('No address found') + + const response = await fetch( + `${MOONPAY_WORKER_URL[chainId]}/transactionInfo?externalTransactionId=${externalTransactionId}`, + ) + const jsonResult = (await response.json()) as Array<{ status: MoonpayTransactionStatus }> + const result = jsonResult?.[0] + + if (result?.status === 'completed') { + useTransactionManager.getState().onRegistrationMoonpayTransactionCompleted(name, { + sourceChainId: chainId, + account: address, + }) + } + + return result || {} + } + +export const useMoonpayRegistration = (normalisedName: string) => { + const [hasMoonpayModal, setHasMoonpayModal] = useState(false) + + const currentExternalTransactionId = useTransactionManager( + (s) => s.getCurrentRegistrationFlowOrDefault(normalisedName).externalTransactionData?.id, + ) + const setRegistrationExternalTransactionData = useTransactionManager( + (s) => s.setRegistrationExternalTransactionData, + ) + + const initiateMoonpayRegistrationMutation = useMutation({ + mutationFn: initiateMoonpayRegistrationMutationFn, + onSuccess: (data, variables) => { + setRegistrationExternalTransactionData( + variables.name, + { + id: data.externalTransactionId, + url: data.moonpayUrl, + type: 'moonpay', + }, + { + sourceChainId: variables.chainId, + account: variables.address, + }, + ) + }, + }) + + const { queryKey, queryFn } = useQueryOptions({ + params: { name: normalisedName, externalTransactionId: currentExternalTransactionId! }, + functionName: 'getMoonpayStatus', + queryDependencyType: 'standard', + queryFn: getMoonpayStatusQueryFn, + }) + + // Monitor current transaction + const { data: transactionData } = useQuery({ + queryKey, + queryFn, + refetchOnWindowFocus: true, + refetchOnMount: true, + refetchInterval: 1000, + refetchIntervalInBackground: true, + enabled: !!currentExternalTransactionId, + }) + + return { + initiateMoonpayRegistrationMutation, + hasMoonpayModal, + setHasMoonpayModal, + moonpayTransactionStatus: transactionData?.status as MoonpayTransactionStatus, + } +} diff --git a/src/constants/chains.ts b/src/constants/chains.ts index 3be53d40e..74ad08158 100644 --- a/src/constants/chains.ts +++ b/src/constants/chains.ts @@ -1,5 +1,5 @@ import { holesky } from 'viem/chains' -import { goerli, localhost, mainnet, sepolia } from 'wagmi/chains' +import { localhost, mainnet, sepolia } from 'wagmi/chains' import { addEnsContracts } from '@ensdomains/ensjs' @@ -25,24 +25,41 @@ export const mainnetWithEns = { }, }, } -export const goerliWithEns = addEnsContracts(goerli) export const sepoliaWithEns = addEnsContracts(sepolia) export const holeskyWithEns = addEnsContracts(holesky) export const chainsWithEns = [ mainnetWithEns, - goerliWithEns, sepoliaWithEns, holeskyWithEns, localhostWithEns, ] as const -export const getSupportedChainById = (chainId: number | undefined) => - chainId ? chainsWithEns.find((c) => c.id === chainId) : undefined +export type GetSupportedChainById = Extract< + SupportedChain, + { id: chainId } +> + +export function getSupportedChainById( + chainId: chainId, +): GetSupportedChainById +export function getSupportedChainById(chainId: number | undefined): SupportedChain | undefined +export function getSupportedChainById(chainId: number | undefined) { + if (!chainId) return undefined + return chainsWithEns.find((c) => c.id === chainId) +} + +export type SourceChain = + | typeof mainnetWithEns + | typeof sepoliaWithEns + | typeof holeskyWithEns + | typeof localhostWithEns + +// this will include L2s later +export type TargetChain = SourceChain export type SupportedChain = | typeof mainnetWithEns - | typeof goerliWithEns | typeof sepoliaWithEns | typeof holeskyWithEns | typeof localhostWithEns diff --git a/src/constants/verification.ts b/src/constants/verification.ts index 2ea762961..b244aa714 100644 --- a/src/constants/verification.ts +++ b/src/constants/verification.ts @@ -1,4 +1,4 @@ -import type { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import type { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' /** * General Verification Constants diff --git a/src/hooks/chain/useChainName.ts b/src/hooks/chain/useChainName.ts index 2f7f78f47..9703baa8a 100644 --- a/src/hooks/chain/useChainName.ts +++ b/src/hooks/chain/useChainName.ts @@ -1,14 +1,13 @@ import { useMemo } from 'react' -import { useChainId, useConfig } from 'wagmi' +import { useChainId } from 'wagmi' import { getChainName } from '@app/utils/getChainName' export const useChainName = () => { - const config = useConfig() const chainId = useChainId() return useMemo(() => { - return getChainName(config, { chainId }) + return getChainName(chainId) // eslint-disable-next-line react-hooks/exhaustive-deps }, [chainId]) } diff --git a/src/hooks/chain/useEstimateGasWithStateOverride.ts b/src/hooks/chain/useEstimateGasWithStateOverride.ts index 34c23b8e4..30f4cf969 100644 --- a/src/hooks/chain/useEstimateGasWithStateOverride.ts +++ b/src/hooks/chain/useEstimateGasWithStateOverride.ts @@ -21,9 +21,9 @@ import { useConnectorClient } from 'wagmi' import { useQueryOptions } from '@app/hooks/useQueryOptions' import { createTransactionRequest, - TransactionName, - TransactionParameters, -} from '@app/transaction-flow/transaction' + type UserTransactionName, + type UserTransactionParameters, +} from '@app/transaction/user/transaction' import { ConfigWithEns, ConnectorClientWithEns, @@ -77,11 +77,14 @@ type StateOverride = { } type TransactionItem = { - [TName in TransactionName]: Omit, 'client' | 'connectorClient'> & { - name: TName + [name in UserTransactionName]: Omit< + UserTransactionParameters, + 'client' | 'connectorClient' + > & { + name: name stateOverride?: UserStateOverrides } -}[TransactionName] +}[UserTransactionName] type UseEstimateGasWithStateOverrideParameters< TransactionItems extends TransactionItem[] | readonly TransactionItem[], @@ -150,13 +153,13 @@ export const addStateOverride = < stateOverride, }) as Prettify -const estimateIndividualGas = async ({ +const estimateIndividualGas = async ({ data, name, stateOverride, connectorClient, client, -}: { name: TName; stateOverride?: UserStateOverrides } & TransactionParameters) => { +}: { name: name; stateOverride?: UserStateOverrides } & UserTransactionParameters) => { const generatedRequest = await createTransactionRequest({ client, connectorClient, diff --git a/src/hooks/gasEstimation/useEstimateRegistration.ts b/src/hooks/gasEstimation/useEstimateRegistration.ts index 3d969e678..78294b168 100644 --- a/src/hooks/gasEstimation/useEstimateRegistration.ts +++ b/src/hooks/gasEstimation/useEstimateRegistration.ts @@ -3,7 +3,7 @@ import { parseEther } from 'viem' import { makeCommitment } from '@ensdomains/ensjs/utils' -import { RegistrationReducerDataItem } from '@app/components/pages/profile/[name]/registration/types' +import type { StoredRegistrationFlow } from '@app/transaction/slices/createRegistrationFlowSlice' import { deriveYearlyFee } from '@app/utils/utils' import { useAccountSafely } from '../account/useAccountSafely' @@ -15,7 +15,7 @@ import { usePrice } from '../ensjs/public/usePrice' import useRegistrationParams from '../useRegistrationParams' type UseEstimateFullRegistrationParameters = { - registrationData: RegistrationReducerDataItem + registrationData: StoredRegistrationFlow name: string } diff --git a/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts b/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts index a998d9f8b..c1056e393 100644 --- a/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts +++ b/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts @@ -13,10 +13,9 @@ import { useWrapperData } from '@app/hooks/ensjs/public/useWrapperData' import { useGetPrimaryNameTransactionFlowItem } from '@app/hooks/primary/useGetPrimaryNameTransactionFlowItem' import { useResolverStatus } from '@app/hooks/resolver/useResolverStatus' import { useProfile } from '@app/hooks/useProfile' -import { makeIntroItem } from '@app/transaction-flow/intro' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' -import { GenericTransaction } from '@app/transaction-flow/types' +import { useTransactionManager } from '@app/transaction/transactionManager' +import { usePreparedDataInput } from '@app/transaction/usePreparedDataInput' +import { createUserTransaction } from '@app/transaction/user/transaction' import { checkAvailablePrimaryName } from '@app/utils/checkAvailablePrimaryName' import { nameParts } from '@app/utils/name' import { useHasGraphError } from '@app/utils/SyncProvider/SyncProvider' @@ -77,7 +76,7 @@ const verificationsButtonTooltip = ({ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => { const { t } = useTranslation('profile') - const { createTransactionFlow, usePreparedDataInput } = useTransactionFlow() + const startFlow = useTransactionManager((s) => s.startFlow) const { address } = useAccountSafely() @@ -174,9 +173,9 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => }) } - const transactionFlowItem = getPrimaryNameTransactionFlowItem?.callBack?.(name) - if (isAvailablePrimaryName && !!transactionFlowItem) { - const key = `setPrimaryName-${name}-${address}` + const flow = getPrimaryNameTransactionFlowItem?.callBack?.(name) + if (isAvailablePrimaryName && !!flow) { + const flowId = `setPrimaryName-${name}-${address}` actions.push({ label: t('tabs.profile.actions.setAsPrimaryName.label'), tooltipContent: hasGraphError @@ -186,12 +185,11 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => loading: hasGraphErrorLoading, onClick: !checkIsDecrypted(name) ? () => - showUnknownLabelsInput(key, { + showUnknownLabelsInput(flowId, { name, - key, - transactionFlowItem, + flow: { ...flow, flowId }, }) - : () => createTransactionFlow(key, transactionFlowItem), + : () => startFlow({ ...flow, flowId }), }) } @@ -227,13 +225,13 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => loading: hasGraphErrorLoading, } if (abilities.canDeleteRequiresWrap) { - const transactions: GenericTransaction[] = [ - createTransactionItem('transferSubname', { + const transactions = [ + createUserTransaction('transferSubname', { name, contract: 'nameWrapper', newOwnerAddress: address, }), - createTransactionItem('deleteSubname', { + createUserTransaction('deleteSubname', { contract: 'nameWrapper', name, method: 'setRecord', @@ -242,16 +240,20 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => actions.push({ ...base, onClick: () => - createTransactionFlow(`deleteSubname-${name}`, { + startFlow({ + flowId: `deleteSubname-${name}`, transactions, resumable: true, intro: { title: ['intro.multiStepSubnameDelete.title', { ns: 'transactionFlow' }], - content: makeIntroItem('GenericWithDescription', { - description: t('intro.multiStepSubnameDelete.description', { - ns: 'transactionFlow', - }), - }), + content: { + name: 'GenericWithDescription', + data: { + description: t('intro.multiStepSubnameDelete.description', { + ns: 'transactionFlow', + }), + }, + }, }, }), }) @@ -278,13 +280,17 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => actions.push({ ...base, onClick: () => - createTransactionFlow(`deleteSubname-${name}`, { + startFlow({ + flowId: `deleteSubname-${name}`, transactions: [ - createTransactionItem('deleteSubname', { - name, - contract: abilities.canDeleteContract!, - method: abilities.canDeleteMethod, - }), + { + name: 'deleteSubname', + data: { + name, + contract: abilities.canDeleteContract!, + method: abilities.canDeleteMethod, + }, + }, ], }), }) @@ -309,13 +315,17 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => fullMobileWidth: true, loading: hasGraphErrorLoading, onClick: () => { - createTransactionFlow(`reclaim-${name}`, { + startFlow({ + flowId: `reclaim-${name}`, transactions: [ - createTransactionItem('createSubname', { - contract: 'nameWrapper', - label, - parent, - }), + { + name: 'createSubname', + data: { + contract: 'nameWrapper', + label, + parent, + }, + }, ], }) }, @@ -327,16 +337,18 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => }, [ address, isLoading, + ownerData?.owner, + ownerData?.registrant, getPrimaryNameTransactionFlowItem, name, isAvailablePrimaryName, - abilities.canEdit, - abilities.canEditRecords, - abilities.canEditResolver, abilities.canDelete, abilities.canDeleteContract, abilities.canDeleteError, abilities.canReclaim, + abilities.canEdit, + abilities.canEditRecords, + abilities.canEditResolver, abilities.canDeleteRequiresWrap, abilities.isPCCBurned, abilities.isParentOwner, @@ -344,14 +356,12 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => t, hasGraphError, hasGraphErrorLoading, - ownerData?.owner, - ownerData?.registrant, + showVerifyProfileInput, showUnknownLabelsInput, - createTransactionFlow, + startFlow, showProfileEditorInput, showDeleteEmancipatedSubnameWarningInput, showDeleteSubnameNotParentWarningInput, - showVerifyProfileInput, ]) return { diff --git a/src/hooks/primary/useGetPrimaryNameTransactionFlowItem/index.ts b/src/hooks/primary/useGetPrimaryNameTransactionFlowItem/index.ts index b0d111947..6c34eed23 100644 --- a/src/hooks/primary/useGetPrimaryNameTransactionFlowItem/index.ts +++ b/src/hooks/primary/useGetPrimaryNameTransactionFlowItem/index.ts @@ -5,9 +5,11 @@ import type { Address } from 'viem' import { useContractAddress } from '@app/hooks/chain/useContractAddress' import type { useResolverStatus } from '@app/hooks/resolver/useResolverStatus' import { useReverseRegistryName } from '@app/hooks/reverseRecord/useReverseRegistryName' -import { makeIntroItem } from '@app/transaction-flow/intro/index' -import { createTransactionItem, TransactionItem } from '@app/transaction-flow/transaction' -import { TransactionIntro } from '@app/transaction-flow/types' +import { createTransactionIntro, type TransactionIntro } from '@app/transaction/user/intro' +import { + createUserTransaction, + type GenericUserTransaction, +} from '@app/transaction/user/transaction' import { emptyAddress } from '@app/utils/constants' import { @@ -51,9 +53,9 @@ export const useGetPrimaryNameTransactionFlowItem = ( return (name: string) => { let introType: IntroType = 'updateEthAddress' const transactions: ( - | TransactionItem<'setPrimaryName'> - | TransactionItem<'updateResolver'> - | TransactionItem<'updateEthAddress'> + | GenericUserTransaction<'setPrimaryName'> + | GenericUserTransaction<'updateResolver'> + | GenericUserTransaction<'updateEthAddress'> )[] = [] if ( @@ -62,7 +64,7 @@ export const useGetPrimaryNameTransactionFlowItem = ( name, }) ) { - transactions.push(createTransactionItem('setPrimaryName', { name, address })) + transactions.push(createUserTransaction('setPrimaryName', { name, address })) } if ( @@ -75,7 +77,7 @@ export const useGetPrimaryNameTransactionFlowItem = ( introType = !resolverAddress || resolverAddress === emptyAddress ? 'noResolver' : 'invalidResolver' transactions.unshift( - createTransactionItem('updateResolver', { + createUserTransaction('updateResolver', { name, contract: isWrapped ? 'nameWrapper' : 'registry', resolverAddress: latestResolverAddress, @@ -92,7 +94,7 @@ export const useGetPrimaryNameTransactionFlowItem = ( }) ) { transactions.unshift( - createTransactionItem('updateEthAddress', { + createUserTransaction('updateEthAddress', { name, address, latestResolver: !resolverStatus?.isAuthorized, @@ -103,13 +105,13 @@ export const useGetPrimaryNameTransactionFlowItem = ( const introItem = transactions.length > 1 ? { - resumeable: true, + resumable: true, intro: { title: [getIntroTranslation(introType, 'title'), { ns: 'transactionFlow' }], - content: makeIntroItem('GenericWithDescription', { + content: createTransactionIntro('GenericWithDescription', { description: t(getIntroTranslation(introType, 'description')), }), - } as TransactionIntro, + } satisfies TransactionIntro, } : {} diff --git a/src/hooks/registration/useExistingCommitment.ts b/src/hooks/registration/useExistingCommitment.ts deleted file mode 100644 index 5af4e5f8a..000000000 --- a/src/hooks/registration/useExistingCommitment.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { QueryFunctionContext, useQuery } from '@tanstack/react-query' -import { - decodeFunctionData, - encodeFunctionData, - getAddress, - Hash, - Hex, - toFunctionSelector, -} from 'viem' -import { getBlock, getTransactionReceipt, readContract } from 'viem/actions' - -import { - ethRegistrarControllerCommitmentsSnippet, - ethRegistrarControllerCommitSnippet, - getChainContractAddress, -} from '@ensdomains/ensjs/contracts' - -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' -import { ConfigWithEns, CreateQueryKey, QueryConfig } from '@app/types' -import { getIsCachedData } from '@app/utils/getIsCachedData' -import { prepareQueryOptions } from '@app/utils/prepareQueryOptions' - -import { useInvalidateOnBlock } from '../chain/useInvalidateOnBlock' -import { useAddRecentTransaction } from '../transactions/useAddRecentTransaction' -import { useIsSafeApp } from '../useIsSafeApp' -import { useQueryOptions } from '../useQueryOptions' -import { getBlockMetadataByTimestamp } from './utils/getBlockMetadataByTimestamp' - -type UseExistingCommitmentParameters = { - commitment?: Hex - commitKey?: string -} - -type UseExistingCommitmentInternalParameters = { - setTransactionHashFromUpdate: (key: string, hash: Hash) => void - addRecentTransaction: ReturnType - isSafeTx: boolean -} - -type UseExistingCommitmentReturnType = - | { - status: 'transactionExists' - timestamp: number - } - | { - status: 'commitmentExists' - timestamp: number - } - | { - status: 'commitmentExpired' - timestamp: number - } - | null - -type UseExistingCommitmentConfig = QueryConfig - -type QueryKey = CreateQueryKey< - TParams, - 'getExistingCommitment', - 'standard' -> - -const maxCommitmentAgeSnippet = [ - { - inputs: [], - name: 'maxCommitmentAge', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, -] as const - -const getCurrentBlockTimestampSnippet = [ - { - inputs: [], - name: 'getCurrentBlockTimestamp', - outputs: [ - { - internalType: 'uint256', - name: 'timestamp', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, -] as const - -const execTransactionSnippet = [ - { - inputs: [ - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'value', type: 'uint256' }, - { internalType: 'bytes', name: 'data', type: 'bytes' }, - { - internalType: 'enum Enum.Operation', - name: 'operation', - type: 'uint8', - }, - { internalType: 'uint256', name: 'safeTxGas', type: 'uint256' }, - { internalType: 'uint256', name: 'baseGas', type: 'uint256' }, - { internalType: 'uint256', name: 'gasPrice', type: 'uint256' }, - { internalType: 'address', name: 'gasToken', type: 'address' }, - { - internalType: 'address payable', - name: 'refundReceiver', - type: 'address', - }, - { internalType: 'bytes', name: 'signatures', type: 'bytes' }, - ], - name: 'execTransaction', - outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }], - stateMutability: 'payable', - type: 'function', - }, -] as const - -const getExistingCommitmentQueryFn = - (config: ConfigWithEns) => - ({ - addRecentTransaction, - setTransactionHashFromUpdate, - isSafeTx, - }: UseExistingCommitmentInternalParameters) => - async ({ - queryKey: [{ commitment, commitKey }, chainId, address], - }: QueryFunctionContext>): Promise => { - if (!commitment) throw new Error('commitment is required') - if (!commitKey) throw new Error('commitKey is required') - if (!address) throw new Error('address is required') - - const client = config.getClient({ chainId }) - const ethRegistrarControllerAddress = getChainContractAddress({ - client, - contract: 'ensEthRegistrarController', - }) - const multicall3Address = getChainContractAddress({ - client, - contract: 'multicall3', - }) - - const [commitmentTimestamp, maxCommitmentAge, blockTimestamp] = await Promise.all([ - readContract(client, { - abi: ethRegistrarControllerCommitmentsSnippet, - address: ethRegistrarControllerAddress, - functionName: 'commitments', - args: [commitment], - }), - readContract(client, { - abi: maxCommitmentAgeSnippet, - address: ethRegistrarControllerAddress, - functionName: 'maxCommitmentAge', - }), - readContract(client, { - abi: getCurrentBlockTimestampSnippet, - address: multicall3Address, - functionName: 'getCurrentBlockTimestamp', - }), - ]) - if (!commitmentTimestamp || commitmentTimestamp === 0n) return null - - const commitmentAge = blockTimestamp - commitmentTimestamp - const commitmentTimestampNumber = Number(commitmentTimestamp) - const existsFailure = () => - ({ status: 'commitmentExists', timestamp: commitmentTimestampNumber }) as const - - if (commitmentAge > maxCommitmentAge) - return { status: 'commitmentExpired', timestamp: commitmentTimestampNumber } as const - - const blockMetadata = await getBlockMetadataByTimestamp(client, { - timestamp: commitmentTimestamp, - }) - if (!blockMetadata.ok) return existsFailure() - - const blockData = await getBlock(client, { - blockHash: blockMetadata.data.hash, - includeTransactions: true, - }).catch(() => null) - if (!blockData) return existsFailure() - - const inputData = encodeFunctionData({ - abi: ethRegistrarControllerCommitSnippet, - args: [commitment], - functionName: 'commit', - }) - - const transaction = (() => { - const checksummedAddress = getAddress(address) - const checksummedEthRegistrarControllerAddress = getAddress(ethRegistrarControllerAddress) - if (isSafeTx) { - const execTransactionFunctionSelector = toFunctionSelector(execTransactionSnippet[0]) - const foundTransaction = blockData.transactions.find((t) => { - // safe transaction gets sent to the safe contract itself - if (!t.to || getAddress(t.to) !== checksummedAddress) return false - if (!t.input.startsWith(execTransactionFunctionSelector)) return false - const { args: safeTxData } = decodeFunctionData({ - abi: execTransactionSnippet, - data: t.input, - }) - if (getAddress(safeTxData[0]) !== checksummedEthRegistrarControllerAddress) return false - if (getAddress(safeTxData[2]) !== inputData) return false - return true - }) - return foundTransaction - } - const foundTransaction = blockData.transactions.find((t) => { - if (getAddress(t.from) !== checksummedAddress) return false - if (!t.to || getAddress(t.to) !== checksummedEthRegistrarControllerAddress) return false - if (t.input !== inputData) return false - return true - }) - return foundTransaction - })() - - if (!transaction) return existsFailure() - - const transactionReceipt = await getTransactionReceipt(client, { - hash: transaction.hash, - }) - - if (transactionReceipt.status !== 'success') return existsFailure() - - setTransactionHashFromUpdate(commitKey, transaction.hash) - addRecentTransaction({ - ...transaction, - hash: transaction.hash, - action: 'commitName', - key: commitKey, - input: inputData, - timestamp: commitmentTimestampNumber, - isSafeTx, - searchRetries: 0, - }) - - return { - status: 'transactionExists', - timestamp: commitmentTimestampNumber, - } as const - } - -export const useExistingCommitment = ({ - // config - enabled = true, - gcTime, - staleTime, - scopeKey, - // params - ...params -}: TParams & UseExistingCommitmentConfig) => { - const initialOptions = useQueryOptions({ - params, - scopeKey, - functionName: 'getExistingCommitment', - queryDependencyType: 'standard', - queryFn: getExistingCommitmentQueryFn, - }) - - const addRecentTransaction = useAddRecentTransaction() - const { setTransactionHashFromUpdate } = useTransactionFlow() - const { data: isSafeApp, isLoading: isSafeAppLoading } = useIsSafeApp() - - if (process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_ETH_NODE === 'anvil') - console.log('commit is:', params.commitment) - - const preparedOptions = prepareQueryOptions({ - queryKey: initialOptions.queryKey, - queryFn: initialOptions.queryFn({ - addRecentTransaction, - setTransactionHashFromUpdate, - isSafeTx: !!isSafeApp, - }), - enabled: enabled && !!params.commitment && !isSafeAppLoading, - gcTime, - staleTime, - }) - - useInvalidateOnBlock({ - enabled: preparedOptions.enabled, - queryKey: preparedOptions.queryKey, - }) - - const query = useQuery(preparedOptions) - - return { - ...query, - isCachedData: getIsCachedData(query), - } -} diff --git a/src/hooks/useEthPrice.ts b/src/hooks/useEthPrice.ts index 4fd8cef70..a4c81090c 100644 --- a/src/hooks/useEthPrice.ts +++ b/src/hooks/useEthPrice.ts @@ -1,21 +1,16 @@ import { Address } from 'viem' -import { useChainId, useReadContract } from 'wagmi' -import { goerli } from 'wagmi/chains' +import { useReadContract } from 'wagmi' import { useAddressRecord } from './ensjs/public/useAddressRecord' const ORACLE_ENS = 'eth-usd.data.eth' -const ORACLE_GOERLI = '0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e' as const export const useEthPrice = () => { - const chainId = useChainId() - - const { data: address_ } = useAddressRecord({ + const { data: addressResult } = useAddressRecord({ name: ORACLE_ENS, - enabled: chainId !== goerli.id, }) - const address = chainId === 5 ? ORACLE_GOERLI : (address_?.value as Address) || undefined + const address = (addressResult?.value as Address) ?? undefined return useReadContract({ abi: [ diff --git a/src/hooks/useFaucet.ts b/src/hooks/useFaucet.ts index 5c9b782d8..59496792e 100644 --- a/src/hooks/useFaucet.ts +++ b/src/hooks/useFaucet.ts @@ -47,11 +47,11 @@ const createEndpoint = (chainName: string) => type QueryKey = CreateQueryKey<{}, 'getFaucetAddress', 'standard'> const getFaucetQueryFn = - (config: ConfigWithEns) => + (_config: ConfigWithEns) => async ({ queryKey: [, chainId, address] }: QueryFunctionContext) => { if (!address) throw new Error('address is required') - const chainName = getChainName(config, { chainId }) + const chainName = getChainName(chainId) const result: JsonRpc<{ eligible: boolean @@ -98,7 +98,7 @@ const useFaucet = () => { const { data, error, isLoading } = useQuery({ ...preparedOptions, - enabled: !!address && (chainName === 'goerli' || chainName === 'sepolia'), + enabled: !!address && chainName === 'sepolia', }) const mutation = useMutation({ diff --git a/src/hooks/useProfileEditorForm.tsx b/src/hooks/useProfileEditorForm.tsx index 69e77dc6a..398421142 100644 --- a/src/hooks/useProfileEditorForm.tsx +++ b/src/hooks/useProfileEditorForm.tsx @@ -5,7 +5,7 @@ import { isEthAddressRecord, profileEditorFormToProfileRecords, profileRecordsToProfileEditorForm, -} from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +} from '@app/components/pages/register/steps/Profile/profileRecordUtils' import { ProfileRecord, ProfileRecordGroup } from '@app/constants/profileRecordOptions' import { supportedAddresses } from '@app/constants/supportedAddresses' import { AvatarEditorType } from '@app/types' diff --git a/src/hooks/useRegistrationParams.ts b/src/hooks/useRegistrationParams.ts index b64b4ded6..6e1aba218 100644 --- a/src/hooks/useRegistrationParams.ts +++ b/src/hooks/useRegistrationParams.ts @@ -3,14 +3,14 @@ import { Address } from 'viem' import { ChildFuseReferenceType, RegistrationParameters } from '@ensdomains/ensjs/utils' -import { profileRecordsToRecordOptions } from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' -import { RegistrationReducerDataItem } from '@app/components/pages/profile/[name]/registration/types' +import { profileRecordsToRecordOptions } from '@app/components/pages/register/steps/Profile/profileRecordUtils' +import type { StoredRegistrationFlow } from '@app/transaction/slices/createRegistrationFlowSlice' type Props = { name: string owner: Address registrationData: Pick< - RegistrationReducerDataItem, + StoredRegistrationFlow, | 'seconds' | 'resolverAddress' | 'secret' @@ -21,28 +21,33 @@ type Props = { > } +export const getRegistrationParams = ({ + name, + owner, + registrationData, +}: Props): RegistrationParameters => { + return { + name, + owner, + duration: registrationData.seconds, + resolverAddress: registrationData.resolverAddress ?? undefined, + secret: registrationData.secret, + records: profileRecordsToRecordOptions(registrationData.records, registrationData.clearRecords), + fuses: { + named: registrationData.permissions + ? (Object.keys(registrationData.permissions).filter( + (key) => !!registrationData.permissions?.[key as ChildFuseReferenceType['Key']], + ) as ChildFuseReferenceType['Key'][]) + : [], + unnamed: [], + }, + reverseRecord: registrationData.reverseRecord, + } +} + const useRegistrationParams = ({ name, owner, registrationData }: Props) => { const registrationParams: RegistrationParameters = useMemo( - () => ({ - name, - owner, - duration: registrationData.seconds, - resolverAddress: registrationData.resolverAddress, - secret: registrationData.secret, - records: profileRecordsToRecordOptions( - registrationData.records, - registrationData.clearRecords, - ), - fuses: { - named: registrationData.permissions - ? (Object.keys(registrationData.permissions).filter( - (key) => !!registrationData.permissions?.[key as ChildFuseReferenceType['Key']], - ) as ChildFuseReferenceType['Key'][]) - : [], - unnamed: [], - }, - reverseRecord: registrationData.reverseRecord, - }), + () => getRegistrationParams({ name, owner, registrationData }), [owner, name, registrationData], ) diff --git a/src/hooks/useRegistrationReducer.ts b/src/hooks/useRegistrationReducer.ts deleted file mode 100644 index f91f6fd5c..000000000 --- a/src/hooks/useRegistrationReducer.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { useChainId } from 'wagmi' - -import { randomSecret } from '@ensdomains/ensjs/utils' - -import { childFuseObj } from '@app/components/@molecules/BurnFuses/BurnFusesContent' -import { - RegistrationReducerAction, - RegistrationReducerData, - RegistrationReducerDataItem, - SelectedItemProperties, -} from '@app/components/pages/profile/[name]/registration/types' -import { useLocalStorageReducer } from '@app/hooks/useLocalStorage' -import { yearsToSeconds } from '@app/utils/utils' - -const REGISTRATION_REDUCER_DATA_ITEM_VERSION = 3 - -const defaultData: RegistrationReducerDataItem = { - stepIndex: 0, - queue: ['pricing', 'info', 'transactions', 'complete'], - seconds: yearsToSeconds(1), - reverseRecord: false, - records: [], - clearRecords: false, - resolverAddress: '0x', - permissions: childFuseObj, - secret: '0x', - started: false, - address: '0x', - name: '', - isMoonpayFlow: false, - externalTransactionId: '', - chainId: 1, - durationType: 'years', - version: REGISTRATION_REDUCER_DATA_ITEM_VERSION, -} - -const isBrowser = !!( - typeof window !== 'undefined' && - window.document && - window.document.createElement -) - -const makeDefaultData = (selected: SelectedItemProperties): RegistrationReducerDataItem => ({ - stepIndex: 0, - queue: ['pricing', 'info', 'transactions', 'complete'], - seconds: yearsToSeconds(1), - reverseRecord: false, - records: [], - resolverAddress: '0x', - permissions: childFuseObj, - secret: randomSecret({ platformDomain: 'enslabs.eth', campaign: 3 }), - started: false, - isMoonpayFlow: false, - externalTransactionId: '', - version: REGISTRATION_REDUCER_DATA_ITEM_VERSION, - durationType: 'years', - ...selected, -}) - -export const getSelectedIndex = ( - state: RegistrationReducerData, - selected: SelectedItemProperties, -) => - state.items.findIndex( - (x) => - x.address === selected.address && - x.name === selected.name && - x.chainId === selected.chainId && - x.version === REGISTRATION_REDUCER_DATA_ITEM_VERSION, - ) - -/* eslint-disable no-param-reassign */ -const reducer = (state: RegistrationReducerData, action: RegistrationReducerAction) => { - let selectedItemInx = getSelectedIndex(state, action.selected) - - if (!isBrowser) return state - - if (selectedItemInx === -1) { - selectedItemInx = state.items.push(makeDefaultData(action.selected)) - 1 - } - - const item = state.items[selectedItemInx] - - switch (action.name) { - case 'clearItem': { - state.items.splice(selectedItemInx, 1) - break - } - case 'resetItem': { - state.items[selectedItemInx] = makeDefaultData(action.selected) - break - } - case 'resetSecret': { - item.secret = randomSecret() - break - } - case 'setQueue': { - item.queue = action.payload - break - } - case 'decreaseStep': { - item.stepIndex -= 1 - break - } - case 'increaseStep': { - item.stepIndex += 1 - break - } - case 'setPricingData': { - item.seconds = action.payload.seconds - item.reverseRecord = action.payload.reverseRecord - item.durationType = action.payload.durationType - break - } - case 'setTransactionsData': { - item.secret = action.payload.secret - item.started = action.payload.started - break - } - case 'setStarted': { - item.started = true - break - } - case 'setProfileData': { - if (action.payload.records) item.records = action.payload.records - if (action.payload.permissions) item.permissions = action.payload.permissions - if (action.payload.resolverAddress) item.resolverAddress = action.payload.resolverAddress - break - } - case 'setExternalTransactionId': { - item.isMoonpayFlow = true - item.externalTransactionId = action.externalTransactionId - break - } - case 'moonpayTransactionCompleted': { - item.externalTransactionId = '' - item.stepIndex = item.queue.findIndex((step) => step === 'complete') - break - } - // no default - } - return state -} -/* eslint-enable no-param-reassign */ - -const useRegistrationReducer = ({ - address, - name, -}: { - address: string | undefined - name: string -}) => { - const chainId = useChainId() - const selected = { address: address!, name, chainId } as const - const [state, dispatch] = useLocalStorageReducer< - RegistrationReducerData, - RegistrationReducerAction - >('registration-status', reducer, { items: [] }) - - let item = defaultData - if (isBrowser) { - const itemIndex = getSelectedIndex(state, selected) - item = itemIndex === -1 ? makeDefaultData(selected) : state.items[itemIndex] - } - - return { state, dispatch, item, selected } -} - -export default useRegistrationReducer diff --git a/src/hooks/useResolverEditor.ts b/src/hooks/useResolverEditor.ts index c123d36b0..13b04d097 100644 --- a/src/hooks/useResolverEditor.ts +++ b/src/hooks/useResolverEditor.ts @@ -17,8 +17,8 @@ export type Props = { } const useResolverEditor = ({ callback, resolverAddress }: Props) => { - const lastestResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) - const isResolverAddressLatest = resolverAddress === lastestResolverAddress + const latestResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + const isResolverAddressLatest = resolverAddress === latestResolverAddress const { register, formState, handleSubmit, reset, trigger, watch, getFieldState, setValue } = useForm({ @@ -43,7 +43,7 @@ const useResolverEditor = ({ callback, resolverAddress }: Props) => { const { resolverChoice: choice, address } = values let newResolver if (choice === 'latest') { - newResolver = lastestResolverAddress + newResolver = latestResolverAddress } if (choice === 'custom') { newResolver = address @@ -61,7 +61,7 @@ const useResolverEditor = ({ callback, resolverAddress }: Props) => { const hasErrors = Object.keys(formState.errors || {}).length > 0 && resolverChoice === 'custom' return { - lastestResolverAddress, + latestResolverAddress, isResolverAddressLatest, register, handleSubmit: handleSubmit(handleResolverSubmit), diff --git a/src/hooks/verification/useVerificationOAuth/useVerificationOAuth.ts b/src/hooks/verification/useVerificationOAuth/useVerificationOAuth.ts index 3c8d33b5d..7fccdefa4 100644 --- a/src/hooks/verification/useVerificationOAuth/useVerificationOAuth.ts +++ b/src/hooks/verification/useVerificationOAuth/useVerificationOAuth.ts @@ -5,7 +5,7 @@ import { getOwner, getRecords } from '@ensdomains/ensjs/public' import { VERIFICATION_RECORD_KEY } from '@app/constants/verification' import { useQueryOptions } from '@app/hooks/useQueryOptions' -import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' import { ConfigWithEns, CreateQueryKey, QueryConfig } from '@app/types' import { prepareQueryOptions } from '@app/utils/prepareQueryOptions' diff --git a/src/hooks/verification/useVerificationOAuthHandler/useVerificationOAuthHandler.ts b/src/hooks/verification/useVerificationOAuthHandler/useVerificationOAuthHandler.ts index b23a2739a..0d6964e31 100644 --- a/src/hooks/verification/useVerificationOAuthHandler/useVerificationOAuthHandler.ts +++ b/src/hooks/verification/useVerificationOAuthHandler/useVerificationOAuthHandler.ts @@ -6,8 +6,7 @@ import { useAccount } from 'wagmi' import type { VerificationErrorDialogProps } from '@app/components/pages/VerificationErrorDialog' import { DENTITY_ISS } from '@app/constants/verification' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' -import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' -import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import type { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' import { useVerificationOAuth } from '../useVerificationOAuth/useVerificationOAuth' import { dentityVerificationHandler } from './utils/dentityHandler' @@ -26,11 +25,10 @@ export const useVerificationOAuthHandler = (): UseVerificationOAuthHandlerReturn const iss = searchParams.get('iss') const code = searchParams.get('code') const router = useRouterWithHistory() - const { createTransactionFlow } = useTransactionFlow() const { address: userAddress } = useAccount() - const isReady = !!createTransactionFlow && !!router && !!userAddress && !!iss && !!code + const isReady = !!router && !!userAddress && !!iss && !!code const { data, isLoading, error } = useVerificationOAuth({ verifier: issToVerificationProtocol(iss), @@ -54,7 +52,6 @@ export const useVerificationOAuthHandler = (): UseVerificationOAuthHandlerReturn onClose, onDismiss, router, - createTransactionFlow, }), ) .otherwise(() => undefined) diff --git a/src/hooks/verification/useVerificationOAuthHandler/utils/createVerificationTransactionFlow.ts b/src/hooks/verification/useVerificationOAuthHandler/utils/createVerificationTransactionFlow.ts index 4a2dd12a0..06576dc4f 100644 --- a/src/hooks/verification/useVerificationOAuthHandler/utils/createVerificationTransactionFlow.ts +++ b/src/hooks/verification/useVerificationOAuthHandler/utils/createVerificationTransactionFlow.ts @@ -1,7 +1,6 @@ import { Hash } from 'viem' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { CreateTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { useTransactionManager } from '@app/transaction/transactionManager' import { UseVerificationOAuthReturnType } from '../../useVerificationOAuth/useVerificationOAuth' @@ -11,7 +10,6 @@ type Props = Pick< > & { userAddress?: Hash router?: any - createTransactionFlow?: CreateTransactionFlow } export const createVerificationTransactionFlow = ({ @@ -19,18 +17,21 @@ export const createVerificationTransactionFlow = ({ verifier, verifiedPresentationUri, resolverAddress, - createTransactionFlow, }: Props) => { - if (!name || !createTransactionFlow || !verifier || !verifiedPresentationUri || !resolverAddress) - return - createTransactionFlow?.(`update-verification-record-${name}`, { + if (!name || !verifier || !verifiedPresentationUri || !resolverAddress) return + + useTransactionManager.getState().startFlow({ + flowId: `update-verification-record-${name}`, transactions: [ - createTransactionItem('updateVerificationRecord', { - name, - verifier, - resolverAddress, - verifiedPresentationUri, - }), + { + name: 'updateVerificationRecord', + data: { + name, + verifier, + resolverAddress, + verifiedPresentationUri, + }, + }, ], }) } diff --git a/src/hooks/verification/useVerificationOAuthHandler/utils/dentityHandler.ts b/src/hooks/verification/useVerificationOAuthHandler/utils/dentityHandler.ts index ff609d27c..d27315c93 100644 --- a/src/hooks/verification/useVerificationOAuthHandler/utils/dentityHandler.ts +++ b/src/hooks/verification/useVerificationOAuthHandler/utils/dentityHandler.ts @@ -7,7 +7,6 @@ import { } from '@app/components/pages/VerificationErrorDialog' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { getDestination } from '@app/routes' -import { CreateTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' import { UseVerificationOAuthReturnType } from '../../useVerificationOAuth/useVerificationOAuth' import { createVerificationTransactionFlow } from './createVerificationTransactionFlow' @@ -37,13 +36,11 @@ export const dentityVerificationHandler = onClose, onDismiss, router, - createTransactionFlow, }: { userAddress?: Hash onClose: () => void onDismiss: () => void router: ReturnType - createTransactionFlow: CreateTransactionFlow }) => (json: UseVerificationOAuthReturnType): VerificationErrorDialogProps => { return match(json) @@ -70,7 +67,6 @@ export const dentityVerificationHandler = resolverAddress, verifier, verifiedPresentationUri, - createTransactionFlow, }) return undefined }, diff --git a/src/hooks/verification/useVerifiedRecords/utils/makeAppendVerificationProps.ts b/src/hooks/verification/useVerifiedRecords/utils/makeAppendVerificationProps.ts index 9172c1a83..b343bbc32 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/makeAppendVerificationProps.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/makeAppendVerificationProps.ts @@ -1,4 +1,4 @@ -import type { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import type { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' import { NormalisedAccountsRecord } from '@app/utils/records/normaliseProfileAccountsRecord' import type { UseVerifiedRecordsReturnType } from '../useVerifiedRecords' diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 3c5d9dfb9..a3d5267ef 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -11,16 +11,14 @@ import { createGlobalStyle, keyframes, ThemeProvider } from 'styled-components' import { ThorinGlobalStyles, lightTheme as thorinLightTheme } from '@ensdomains/thorin' -import { Notifications } from '@app/components/Notifications' +import { Notifications } from '@app/components/Notifications2' import { TestnetWarning } from '@app/components/TestnetWarning' import { TransactionStoreProvider } from '@app/hooks/transactions/TransactionStoreContext' import { Basic } from '@app/layouts/Basic' -import { TransactionFlowProvider } from '@app/transaction-flow/TransactionFlowProvider' +import { TransactionDialogManager } from '@app/transaction/components/TransactionDialogManager' import { setupAnalytics } from '@app/utils/analytics' import { BreakpointProvider } from '@app/utils/BreakpointProvider' import { QueryProviders } from '@app/utils/query/providers' -import { SyncDroppedTransaction } from '@app/utils/SyncProvider/SyncDroppedTransaction' -import { SyncProvider } from '@app/utils/SyncProvider/SyncProvider' import i18n from '../i18n' @@ -151,15 +149,10 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { - - - - - - {getLayout()} - - - + + + + {getLayout()} diff --git a/src/pages/address.tsx b/src/pages/address.tsx index c69cc37ce..07ed71b5f 100644 --- a/src/pages/address.tsx +++ b/src/pages/address.tsx @@ -4,17 +4,17 @@ import { ReactElement, useState } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { Address } from 'viem' +import { useChainId } from 'wagmi' import { NameListView } from '@app/components/@molecules/NameListView/NameListView' import NoProfileSnippet from '@app/components/address/NoProfileSnippet' import { Outlink } from '@app/components/Outlink' import { ProfileSnippet } from '@app/components/ProfileSnippet' -import { useChainName } from '@app/hooks/chain/useChainName' import { usePrimaryProfile } from '@app/hooks/usePrimaryProfile' import { Content } from '@app/layouts/Content' import { ContentGrid } from '@app/layouts/ContentGrid' import { OG_IMAGE_URL } from '@app/utils/constants' -import { makeEtherscanLink, shortenAddress } from '@app/utils/utils' +import { createEtherscanLink, shortenAddress } from '@app/utils/utils' import { useAccountSafely } from '../hooks/account/useAccountSafely' @@ -51,7 +51,7 @@ const Page = () => { const descriptionContent = t('meta.description', { address }) const ogImageUrl = `${OG_IMAGE_URL}/address/${address}` - const chainName = useChainName() + const chainId = useChainId() return ( <> @@ -87,7 +87,10 @@ const Page = () => { ), titleExtra: ( - + {t('etherscan', { ns: 'common' })} ), diff --git a/src/pages/register.tsx b/src/pages/register.tsx index 82d16e95e..e1f4b30e5 100644 --- a/src/pages/register.tsx +++ b/src/pages/register.tsx @@ -1,12 +1,12 @@ import { ReactElement } from 'react' import { useAccount, useChainId } from 'wagmi' -import Registration from '@app/components/pages/profile/[name]/registration/Registration' +import Registration from '@app/components/pages/register/Registration' import { useInitial } from '@app/hooks/useInitial' import { useNameDetails } from '@app/hooks/useNameDetails' -import { getSelectedIndex } from '@app/hooks/useRegistrationReducer' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { ContentGrid } from '@app/layouts/ContentGrid' +import { useTransactionManager } from '@app/transaction/transactionManager' export default function Page() { const router = useRouterWithHistory() @@ -22,27 +22,21 @@ export default function Page() { const nameDetails = useNameDetails({ name }) const { isLoading: detailsLoading, registrationStatus } = nameDetails + const getCurrentRegistrationFlowStep = useTransactionManager( + (s) => s.getCurrentRegistrationFlowStep, + ) + const isLoading = detailsLoading || initial if (!isLoading && registrationStatus !== 'available' && registrationStatus !== 'premium') { let redirect = true if (nameDetails.ownerData?.owner === address && !!address) { - const registrationData = JSON.parse( - localStorage.getItem('registration-status') || '{"items":[]}', - ) - const index = getSelectedIndex(registrationData, { - address: address!, - name: nameDetails.normalisedName, - chainId, + const step = getCurrentRegistrationFlowStep(nameDetails.normalisedName, { + account: address!, + sourceChainId: chainId, }) - if (index !== -1) { - const { stepIndex, queue } = registrationData.items[index] - const step = queue[stepIndex] - if (step === 'transactions' || step === 'complete') { - redirect = false - } - } + if (step === 'transactions' || step === 'complete') redirect = false } if (redirect) { diff --git a/src/transaction-flow/TransactionFlowProvider.tsx b/src/transaction-flow/TransactionFlowProvider.tsx deleted file mode 100644 index 301ecc0f6..000000000 --- a/src/transaction-flow/TransactionFlowProvider.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import React, { - ComponentProps, - ReactNode, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react' -import { Hash } from 'viem' - -import { useAccountSafely } from '@app/hooks/account/useAccountSafely' -import { useLocalStorageReducer } from '@app/hooks/useLocalStorage' -import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' -import { UpdateCallback, useCallbackOnTransaction } from '@app/utils/SyncProvider/SyncProvider' - -import { TransactionDialogManager } from '../components/@molecules/TransactionDialogManager/TransactionDialogManager' -import { DataInputComponent, DataInputComponents } from './input' -import { helpers, initialState, reducer } from './reducer' -import { GenericTransaction, InternalTransactionFlow, TransactionFlowItem } from './types' - -type ShowDataInput = ( - key: string, - data: ComponentProps['data'], - options?: { - disableBackgroundClick?: boolean - }, -) => void - -type UsePreparedDataInput = (name: C) => ShowDataInput - -export type CreateTransactionFlow = (key: string, flow: TransactionFlowItem) => void - -type ProviderValue = { - usePreparedDataInput: UsePreparedDataInput - createTransactionFlow: CreateTransactionFlow - resumeTransactionFlow: (key: string) => void - getTransactionIndex: (key: string) => number - getResumable: (key: string) => boolean - getTransactionFlowStage: ( - key: string, - ) => 'undefined' | 'input' | 'intro' | 'transaction' | 'completed' - getLatestTransaction: (key: string) => GenericTransaction | undefined - stopCurrentFlow: () => void - cleanupFlow: (key: string) => void - setTransactionHashFromUpdate: (key: string, hash: Hash) => void -} - -const TransactionContext = React.createContext({ - usePreparedDataInput: () => () => {}, - createTransactionFlow: () => {}, - resumeTransactionFlow: () => {}, - getTransactionIndex: () => 0, - getResumable: () => false, - getTransactionFlowStage: () => 'undefined', - getLatestTransaction: () => undefined, - stopCurrentFlow: () => {}, - cleanupFlow: () => {}, - setTransactionHashFromUpdate: () => {}, -}) - -export const TransactionFlowProvider = ({ children }: { children: ReactNode }) => { - const router = useRouterWithHistory() - - const [state, dispatch] = useLocalStorageReducer( - 'tx-flow', - reducer, - initialState, - (current: InternalTransactionFlow) => { - const updatedItems = current.items - const { getCanRemoveItem } = helpers(current) - // eslint-disable-next-line guard-for-in - for (const key in updatedItems) { - const item = updatedItems[key] - if (getCanRemoveItem(item)) { - delete updatedItems[key] - } - } - return { - items: updatedItems, - selectedKey: null, - } - }, - ) - - const getTransactionIndex = useCallback( - (key: string) => state.items[key]?.currentTransaction || 0, - [state.items], - ) - - const getTransactionFlowStage = useCallback( - (key: string) => { - const item = state.items[key] - if (!item) return 'undefined' - if (item.currentFlowStage !== 'transaction') return item.currentFlowStage - const { transactions } = item - if (transactions.length === 0) return 'completed' - const lastTransaction = transactions[transactions.length - 1] - if (lastTransaction.stage === 'complete') return 'completed' - return 'transaction' - }, - [state.items], - ) - - const getTransaction = useCallback( - (key: string) => { - return state.items[key] - }, - [state.items], - ) - - const getResumable = useCallback( - (key: string) => { - const { getSelectedItem, getCanRemoveItem } = helpers({ - selectedKey: key, - items: state.items, - }) - const item = getSelectedItem() - if (!item) return false - if (getCanRemoveItem(item)) return false - return true - }, - [state.items], - ) - - const updateCallback = useCallback( - (transaction) => { - if (transaction.status !== 'pending' && transaction.key) { - dispatch({ - name: 'setTransactionStageFromUpdate', - payload: transaction, - }) - } - }, - [dispatch], - ) - - useCallbackOnTransaction(updateCallback) - - const getLatestTransaction = useCallback( - (key: string) => { - const { getSelectedItem } = helpers({ - selectedKey: key, - items: state.items, - }) - const item = getSelectedItem() - if (!item) return undefined - return item.transactions[item.currentTransaction] - }, - [state.items], - ) - - const setTransactionHashFromUpdate = useCallback( - (key: string, hash: Hash) => { - dispatch({ name: 'setTransactionHashFromUpdate', payload: { key, hash } }) - }, - [dispatch], - ) - - const resumeTransactionFlow = useCallback( - (key: string) => { - dispatch({ name: 'resumeFlowWithCheck', key, payload: { push: router.pushWithHistory } }) - }, - [dispatch, router.pushWithHistory], - ) - - const providerValue: ProviderValue = useMemo(() => { - return { - usePreparedDataInput: (name: C) => { - const { address } = useAccountSafely() - if (address) (DataInputComponents[name] as any).render.preload() - const func: ShowDataInput = (key, data, options = {}) => - dispatch({ - name: 'showDataInput', - payload: { - input: { name, data }, - disableBackgroundClick: options.disableBackgroundClick, - }, - key, - }) - return func - }, - createTransactionFlow: ((key, flow) => - dispatch({ - name: 'startFlow', - key, - payload: flow, - })) as CreateTransactionFlow, - resumeTransactionFlow, - getTransactionIndex, - getTransaction, - getResumable, - getTransactionFlowStage, - getLatestTransaction, - stopCurrentFlow: () => dispatch({ name: 'stopFlow' }), - cleanupFlow: (key: string) => dispatch({ name: 'forceCleanupTransaction', payload: key }), - setTransactionHashFromUpdate, - } - }, [ - dispatch, - resumeTransactionFlow, - getResumable, - getTransactionIndex, - getLatestTransaction, - getTransactionFlowStage, - getTransaction, - setTransactionHashFromUpdate, - ]) - - const [selectedKey, setSelectedKey] = useState(null) - - useEffect(() => { - let timeout: NodeJS.Timeout - if (state.selectedKey) { - setSelectedKey(state.selectedKey) - } else { - timeout = setTimeout(() => { - setSelectedKey((prev) => { - if (prev) dispatch({ name: 'cleanupTransaction', payload: prev }) - return null - }) - }, 350) - } - return () => { - clearTimeout(timeout) - } - }, [state.selectedKey, dispatch]) - - return ( - - {children} - - - ) -} - -export const useTransactionFlow = () => { - const context = useContext(TransactionContext) - return context -} diff --git a/src/transaction-flow/intro/index.ts b/src/transaction-flow/intro/index.ts deleted file mode 100644 index 5f1de3691..000000000 --- a/src/transaction-flow/intro/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ComponentProps } from 'react' - -import { ChangePrimaryName } from './ChangePrimaryName' -import { GenericWithDescription } from './GenericWithDescription' -import { MigrateAndUpdateResolver } from './MigrateAndUpdateResolver' -import { SyncManager } from './SyncManager' -import { WrapName } from './WrapName' - -export const intros = { - WrapName, - MigrateAndUpdateResolver, - SyncManager, - ChangePrimaryName, - GenericWithDescription, -} - -export type IntroComponent = typeof intros -export type IntroComponentName = keyof IntroComponent - -export const makeIntroItem = ( - name: I, - data: ComponentProps, -) => ({ - name, - data, -}) diff --git a/src/transaction-flow/reducer.test.ts b/src/transaction-flow/reducer.test.ts deleted file mode 100644 index 32e32ef98..000000000 --- a/src/transaction-flow/reducer.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' - -import { reducer } from './reducer' -import { InternalTransactionFlow, TransactionFlowAction } from './types' - -describe('reducer', () => { - it('should not break if resumeFlowWithCheck is called with item', () => { - const mockPush = vi.fn() - const action = { - name: 'resumeFlowWithCheck', - key: 'key', - payload: { - push: mockPush, - }, - } as TransactionFlowAction - const draft = { - items: { - key: { - resumeLink: 'resumeLink', - transactions: [{ hash: 'hash', stage: 'complete' }], - }, - }, - } as any - reducer(draft, action) - expect(mockPush).toHaveBeenCalled() - }) - it('should break if resumeFlowWithCheck is called wihout item', () => { - const mockPush = vi.fn() - const action = { - name: 'resumeFlowWithCheck', - key: 'key', - payload: { - push: mockPush, - }, - } as TransactionFlowAction - const draft = { - items: { - otherKey: { - resumeLink: 'resumeLink', - transactions: [{ hash: 'hash', stage: 'complete' }], - }, - }, - } as any - reducer(draft, action) - expect(mockPush).not.toHaveBeenCalled() - }) - it('should not break if resumeFlow is called with item', () => { - const mockPush = vi.fn() - const action = { - name: 'resumeFlow', - key: 'key', - payload: { - push: mockPush, - }, - } as TransactionFlowAction - const draft = { - selectedKey: '', - items: { - key: { - intro: true, - currentFlowStage: '', - }, - }, - } as any - reducer(draft, action) - expect(draft.selectedKey).toEqual('key') - }) - it('should break if resumeFlow is called wihout item', () => { - const mockPush = vi.fn() - const action = { - name: 'resumeFlow', - key: 'key', - payload: { - push: mockPush, - }, - } as TransactionFlowAction - const draft = { - selectedKey: '', - items: { - otherkey: { - intro: true, - currentFlowStage: '', - }, - }, - } as any - reducer(draft, action) - expect(draft.selectedKey).toEqual('') - }) - it('should update existing transaction item for repriced transaction', () => { - const action: TransactionFlowAction = { - name: 'setTransactionStageFromUpdate', - payload: { - hash: 'hash' as any, - key: 'key', - action: 'action', - status: 'repriced', - minedData: { - timestamp: 1000, - } as any, - newHash: 'newHash' as any, - searchRetries: 0, - }, - } - const draft: InternalTransactionFlow = { - selectedKey: '', - items: { - key: { - transactions: [{ name: 'testSendName', hash: 'hash' as any, stage: 'sent', data: {} }], - currentTransaction: 0, - currentFlowStage: 'transaction', - }, - }, - } - reducer(draft, action) - - const transaction = draft.items.key.transactions[0] - expect(transaction.hash).toEqual('newHash') - expect(transaction.stage).toEqual('sent') - }) -}) diff --git a/src/transaction-flow/reducer.ts b/src/transaction-flow/reducer.ts deleted file mode 100644 index 585dbf0c5..000000000 --- a/src/transaction-flow/reducer.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* eslint-disable default-case */ - -/* eslint-disable no-param-reassign */ -import { - InternalTransactionFlow, - InternalTransactionFlowItem, - TransactionFlowAction, - TransactionFlowStage, -} from './types' - -export const initialState: InternalTransactionFlow = { - selectedKey: null, - items: {}, -} - -export const helpers = (draft: InternalTransactionFlow) => { - const getSelectedItem = () => draft.items[draft.selectedKey!] - const getCurrentTransaction = (item: InternalTransactionFlowItem) => - item.transactions[item.currentTransaction] - const getAllTransactionsComplete = (item: InternalTransactionFlowItem) => - item.transactions.every(({ hash, stage }) => hash && stage === 'complete') - const getNoTransactionsStarted = (item: InternalTransactionFlowItem) => - item.transactions.every(({ stage }) => !stage || stage === 'confirm') - const getCanRemoveItem = (item: InternalTransactionFlowItem) => - item.requiresManualCleanup - ? false - : !item.transactions || - !item.resumable || - getAllTransactionsComplete(item) || - getNoTransactionsStarted(item) - - return { - getSelectedItem, - getCurrentTransaction, - getAllTransactionsComplete, - getCanRemoveItem, - } -} - -export const reducer = (draft: InternalTransactionFlow, action: TransactionFlowAction) => { - const { getSelectedItem, getCurrentTransaction, getAllTransactionsComplete } = helpers(draft) - - switch (action.name) { - case 'showDataInput': { - draft.items[action.key] = { - currentFlowStage: 'input', - currentTransaction: 0, - input: action.payload.input, - disableBackgroundClick: action.payload.disableBackgroundClick || undefined, - transactions: [], - } - draft.selectedKey = action.key - break - } - case 'startFlow': { - let currentFlowStage: TransactionFlowStage = 'transaction' - if (action.payload.intro) { - currentFlowStage = 'intro' - } - if (action.payload.input) { - currentFlowStage = 'input' - } - draft.items[action.key] = { - ...action.payload, - currentTransaction: 0, - currentFlowStage, - } - draft.selectedKey = action.key - break - } - case 'resumeFlowWithCheck': { - const { - key, - payload: { push }, - } = action - const item = draft.items[key] - if (!item) break // item no longer exists because transactions were completed - if (item.resumeLink && getAllTransactionsComplete(item)) { - push(item.resumeLink) - break - } - // falls through - } - case 'resumeFlow': { - const { key } = action - const item = draft.items[key] - if (!item) break // item no longer exists because transactions were completed - if (item.intro) { - item.currentFlowStage = 'intro' - } - draft.items[key] = item - draft.selectedKey = key - break - } - case 'setTransactions': { - getSelectedItem().transactions = action.payload - break - } - case 'setFlowStage': { - getSelectedItem().currentFlowStage = action.payload - break - } - case 'stopFlow': { - draft.selectedKey = null - break - } - case 'setFailedTransaction': { - if (!action.payload.key) { - console.error('No key provided for setFailedTransaction') - break - } - const transaction = draft.items[action.payload.key].transactions.find( - (x) => x.hash === action.payload.hash, - ) - if (!transaction) { - console.error('No transaction found for setFailedTransaction') - break - } - transaction.stage = 'failed' - break - } - case 'incrementTransaction': { - getSelectedItem().currentTransaction += 1 - break - } - case 'resetTransactionStep': { - getSelectedItem().currentTransaction = 0 - break - } - case 'setTransactionStage': { - const selectedItem = getSelectedItem() - if (!selectedItem) break - const currentTransaction = getCurrentTransaction(selectedItem) - - currentTransaction.stage = action.payload - break - } - case 'setTransactionHash': { - const { hash, key } = action.payload - const selectedItem = draft.items[key] - if (!selectedItem) break - const currentTransaction = getCurrentTransaction(selectedItem) - - currentTransaction.hash = hash - currentTransaction.stage = 'sent' - currentTransaction.sendTime = Date.now() - break - } - case 'setTransactionHashFromUpdate': { - const { hash, key } = action.payload - const selectedItem = draft.items[key!] - if (!selectedItem) break - const currentTransaction = getCurrentTransaction(selectedItem) || selectedItem.transactions[0] - currentTransaction.hash = hash - currentTransaction.stage = 'sent' - currentTransaction.sendTime = Date.now() - break - } - case 'setTransactionStageFromUpdate': { - const { hash, key, status, minedData, newHash } = action.payload - - const selectedItem = draft.items[key!] - if (!selectedItem) break - const transaction = selectedItem.transactions.find((x) => x.hash === hash) - - if (transaction) { - if (status === 'repriced') { - transaction.hash = newHash - transaction.stage = 'sent' - break - } - const stage = status === 'confirmed' ? 'complete' : 'failed' - transaction.stage = stage - transaction.minedData = minedData - transaction.finaliseTime = minedData?.timestamp - if ( - key === draft.selectedKey && - selectedItem.autoClose && - getAllTransactionsComplete(selectedItem) - ) { - draft.selectedKey = null - } - } - break - } - case 'forceCleanupTransaction': - case 'cleanupTransaction': { - const selectedItem = draft.items[action.payload] - if ( - selectedItem && - (!selectedItem.requiresManualCleanup || action.name === 'forceCleanupTransaction') && - (!selectedItem.resumable || getAllTransactionsComplete(selectedItem)) - ) { - delete draft.items[action.payload] - } - break - } - } -} diff --git a/src/transaction-flow/transaction/index.ts b/src/transaction-flow/transaction/index.ts deleted file mode 100644 index 55211a5e5..000000000 --- a/src/transaction-flow/transaction/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -import approveDnsRegistrar from './approveDnsRegistrar' -import approveNameWrapper from './approveNameWrapper' -import burnFuses from './burnFuses' -import changePermissions from './changePermissions' -import claimDnsName from './claimDnsName' -import commitName from './commitName' -import createSubname from './createSubname' -import deleteSubname from './deleteSubname' -import extendNames from './extendNames' -import importDnsName from './importDnsName' -import migrateProfile from './migrateProfile' -import migrateProfileWithReset from './migrateProfileWithReset' -import registerName from './registerName' -import removeVerificationRecord from './removeVerificationRecord' -import resetPrimaryName from './resetPrimaryName' -import resetProfile from './resetProfile' -import resetProfileWithRecords from './resetProfileWithRecords' -import setPrimaryName from './setPrimaryName' -import syncManager from './syncManager' -import testSendName from './testSendName' -import transferController from './transferController' -import transferName from './transferName' -import transferSubname from './transferSubname' -import unwrapName from './unwrapName' -import updateEthAddress from './updateEthAddress' -import updateProfile from './updateProfile' -import updateProfileRecords from './updateProfileRecords' -import updateResolver from './updateResolver' -import updateVerificationRecord from './updateVerificationRecord' -import wrapName from './wrapName' - -export const transactions = { - approveDnsRegistrar, - approveNameWrapper, - burnFuses, - changePermissions, - claimDnsName, - commitName, - createSubname, - deleteSubname, - extendNames, - importDnsName, - migrateProfile, - migrateProfileWithReset, - registerName, - resetPrimaryName, - resetProfile, - resetProfileWithRecords, - setPrimaryName, - syncManager, - testSendName, - transferController, - transferName, - transferSubname, - unwrapName, - updateEthAddress, - updateProfile, - updateProfileRecords, - updateResolver, - wrapName, - updateVerificationRecord, - removeVerificationRecord, -} - -export type Transaction = typeof transactions -export type TransactionName = keyof Transaction - -export type TransactionParameters = Parameters< - Transaction[T]['transaction'] ->[0] - -export type TransactionData = TransactionParameters['data'] - -export type TransactionReturnType = ReturnType< - Transaction[T]['transaction'] -> - -export const createTransactionItem = ( - name: T, - data: TransactionData, -) => ({ - name, - data, -}) - -export const createTransactionRequest = ({ - name, - ...rest -}: { name: TName } & TransactionParameters): TransactionReturnType => { - // i think this has to be any :( - return transactions[name].transaction({ ...rest } as any) as TransactionReturnType -} - -export type TransactionItem = { - name: TName - data: TransactionData -} - -export type TransactionItemUnion = { - [TName in TransactionName]: TransactionItem -}[TransactionName] diff --git a/src/transaction-flow/types.ts b/src/transaction-flow/types.ts deleted file mode 100644 index ceed4874a..000000000 --- a/src/transaction-flow/types.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { TOptions } from 'i18next' -import { ComponentProps, Dispatch, ReactNode } from 'react' -import { Hash } from 'viem' - -import { Button, Dialog, Helper } from '@ensdomains/thorin' - -import { Transaction } from '@app/hooks/transactions/transactionStore' -import { MinedData, TransactionDisplayItem } from '@app/types' - -import type { DataInputComponent } from './input' -import type { IntroComponentName } from './intro' -import type { TransactionData, TransactionItem, TransactionName } from './transaction' - -export type TransactionFlowStage = 'input' | 'intro' | 'transaction' - -export type TransactionStage = 'confirm' | 'sent' | 'complete' | 'failed' - -type GenericDataInput = { - name: keyof DataInputComponent - data: any -} - -export type GenericTransaction< - TName extends TransactionName = TransactionName, - TData extends TransactionData = TransactionData, -> = { - name: TName - data: TData - hash?: Hash - sendTime?: number - finaliseTime?: number - stage?: TransactionStage - minedData?: MinedData -} - -type GenericIntro = { - name: IntroComponentName - data: any -} - -type StoredTranslationReference = [key: string, options?: TOptions] - -export type TransactionIntro = { - title: StoredTranslationReference - leadingLabel?: StoredTranslationReference - trailingLabel?: StoredTranslationReference - content: GenericIntro -} - -export type TransactionFlowItem = { - input?: GenericDataInput - intro?: TransactionIntro - transactions: readonly GenericTransaction[] | GenericTransaction[] - resumable?: boolean - requiresManualCleanup?: boolean - autoClose?: boolean - resumeLink?: string - disableBackgroundClick?: boolean -} - -export type BaseInternalTransactionFlowItem = TransactionFlowItem & { - currentTransaction: number - currentFlowStage: TransactionFlowStage -} - -export type InternalTransactionFlowItem = - | BaseInternalTransactionFlowItem - | (BaseInternalTransactionFlowItem & { - currentFlowStage: 'input' - input: GenericDataInput - }) - -export type InternalTransactionFlow = { - selectedKey: string | null - items: { [key: string]: InternalTransactionFlowItem } -} - -export type TransactionFlowAction = - | { - name: 'showDataInput' - payload: { - input: GenericDataInput - disableBackgroundClick?: boolean - } - key: string - } - | { - name: 'startFlow' - payload: TransactionFlowItem - key: string - } - | { - name: 'resumeFlow' - key: string - } - | { - name: 'resumeFlowWithCheck' - key: string - payload: { - push: (path: string) => void - } - } - | { - name: 'setTransactions' - payload: { - [key in TransactionName]: { - name: key - data: TransactionData - } - }[TransactionName][] - } - | { - name: 'setFlowStage' - payload: TransactionFlowStage - } - | { - name: 'stopFlow' - } - | { - name: 'setTransactionStage' - payload: TransactionStage - } - | { - name: 'setTransactionHash' - payload: { hash: Hash; key: string } - } - | { - name: 'setTransactionHashFromUpdate' - payload: { hash: Hash; key: string } - } - | { - name: 'incrementTransaction' - } - | { - name: 'cleanupTransaction' - payload: string - } - | { - name: 'forceCleanupTransaction' - payload: string - } - | { - name: 'setTransactionStageFromUpdate' - payload: Transaction - } - | { - name: 'resetTransactionStep' - } - | { - name: 'setFailedTransaction' - payload: Transaction - } - -export type TransactionDialogProps = ComponentProps & { - variant: 'actionable' - children: () => ReactNode - leading: ComponentProps - trailing: ComponentProps -} - -export type TransactionDialogPassthrough = { - dispatch: Dispatch - onDismiss: () => void - transactions?: readonly TransactionItem[] | TransactionItem[] -} - -export type ManagedDialogProps = { - dispatch: Dispatch - onDismiss: () => void - transaction: GenericTransaction - actionName: string - txKey: string | null - currentStep: number - stepCount: number - displayItems: TransactionDisplayItem[] - helper?: ComponentProps - backToInput: boolean -} - -export type GetUniqueTransactionParameters = Pick & { - transaction: Pick -} - -export type UniqueTransaction = { - key: string - step: number - name: TName - data: TransactionData -} diff --git a/src/transaction/analytics.ts b/src/transaction/analytics.ts new file mode 100644 index 000000000..2a0497831 --- /dev/null +++ b/src/transaction/analytics.ts @@ -0,0 +1,12 @@ +import { trackEvent } from '@app/utils/analytics' + +import type { StoredTransaction } from './slices/createTransactionSlice' + +export const onTransactionUpdateAnalytics = ( + transaction: Extract, +) => { + if (!transaction) return + if (transaction.name === 'registerName') trackEvent('register', transaction.targetChainId) + else if (transaction.name === 'commitName') trackEvent('commit', transaction.targetChainId) + else if (transaction.name === 'extendNames') trackEvent('renew', transaction.targetChainId) +} diff --git a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx b/src/transaction/components/DisplayItems.tsx similarity index 100% rename from src/components/@molecules/TransactionDialogManager/DisplayItems.tsx rename to src/transaction/components/DisplayItems.tsx diff --git a/src/components/@molecules/TransactionDialogManager/DynamicLoadingContext.tsx b/src/transaction/components/DynamicLoadingContext.tsx similarity index 100% rename from src/components/@molecules/TransactionDialogManager/DynamicLoadingContext.tsx rename to src/transaction/components/DynamicLoadingContext.tsx diff --git a/src/transaction/components/TransactionDialogManager.tsx b/src/transaction/components/TransactionDialogManager.tsx new file mode 100644 index 000000000..887814d33 --- /dev/null +++ b/src/transaction/components/TransactionDialogManager.tsx @@ -0,0 +1,134 @@ +import { QueryClientProvider } from '@tanstack/react-query' +import { type ComponentType } from 'react' +import { useTranslation } from 'react-i18next' + +import { Dialog } from '@ensdomains/thorin' + +import { queryClientWithRefetch } from '@app/utils/query/reactQuery' + +import type { StoredFlow } from '../slices/createFlowSlice' +import type { StoredTransaction } from '../slices/createTransactionSlice' +import type { AllSlices } from '../slices/types' +import { useTransactionManager } from '../transactionManager' +import { + transactionInputComponents, + type GenericTransactionInput, + type TransactionInputName, +} from '../user/input' +import type { TransactionIntro } from '../user/intro' +import { userTransactions } from '../user/transaction' +import { IntroStageModal } from './stage/intro/IntroStageModal' +import { TransactionStageModal } from './stage/transaction/TransactionStageModal' + +export type TransactionDialogPassthrough = { + onDismiss: () => void + setTransactions: AllSlices['setCurrentFlowTransactions'] + setStage: AllSlices['setCurrentFlowStage'] + transactions?: StoredTransaction[] +} + +const InputContent = ({ + flow, +}: { + flow: StoredFlow & { input: GenericTransactionInput } +}) => { + const transactions = useTransactionManager((s) => s.getFlowTransactions(flow.flowId)) + const onDismiss = useTransactionManager((s) => s.stopCurrentFlow) + const setTransactions = useTransactionManager((s) => s.setCurrentFlowTransactions) + const setStage = useTransactionManager((s) => s.setCurrentFlowStage) + const Component = transactionInputComponents[flow.input.name] as ComponentType< + { data: any } & TransactionDialogPassthrough + > + return ( + + + + ) +} + +const IntroContent = ({ flow }: { flow: StoredFlow & { intro: TransactionIntro } }) => { + const transactions = useTransactionManager((s) => s.getFlowTransactions(flow.flowId)) + const setFlowStage = useTransactionManager((s) => s.setCurrentFlowStage) + const onDismiss = useTransactionManager((s) => s.stopCurrentFlow) + + const currentTransaction = transactions[flow.currentTransactionIndex] + const currentStep = + currentTransaction.status === 'success' + ? flow.currentTransactionIndex + 1 + : flow.currentTransactionIndex + const stepStatus = + currentTransaction.status === 'pending' || currentTransaction.status === 'reverted' + ? 'inProgress' + : 'notStarted' + + return ( + setFlowStage('transaction')} + {...{ + ...flow.intro, + onDismiss, + transactions, + }} + /> + ) +} + +const TransactionContent = ({ flow }: { flow: StoredFlow }) => { + const { t } = useTranslation() + const transactions = useTransactionManager((s) => s.getFlowTransactions(flow.flowId)) + const onDismiss = useTransactionManager((s) => s.stopCurrentFlow) + const currentTransaction = transactions[flow.currentTransactionIndex] + const userTransaction = userTransactions[currentTransaction.name] + + const displayItems = userTransaction.displayItems(currentTransaction.data as never, t) + + return ( + + ) +} + +const Content = ({ flow }: { flow: StoredFlow | null }) => { + if (!flow) return null + + if (flow.input && flow.currentStage === 'input') + return ( + }} + /> + ) + if (flow.intro && flow.currentStage === 'intro') + return + return +} + +export const TransactionDialogManager = () => { + const { flow, isPrevious } = useTransactionManager((s) => s.getCurrentOrPreviousFlow()) + const stopFlow = useTransactionManager((s) => s.stopCurrentFlow) + const attemptDismiss = useTransactionManager((s) => s.attemptCurrentFlowDismiss) + + return ( + + + + ) +} diff --git a/src/transaction-flow/TransactionLoader.tsx b/src/transaction/components/TransactionLoader.tsx similarity index 100% rename from src/transaction-flow/TransactionLoader.tsx rename to src/transaction/components/TransactionLoader.tsx diff --git a/src/components/@molecules/TransactionDialogManager/stage/Intro.tsx b/src/transaction/components/stage/intro/IntroStageModal.tsx similarity index 85% rename from src/components/@molecules/TransactionDialogManager/stage/Intro.tsx rename to src/transaction/components/stage/intro/IntroStageModal.tsx index 363ad7996..4ff365054 100644 --- a/src/components/@molecules/TransactionDialogManager/stage/Intro.tsx +++ b/src/transaction/components/stage/intro/IntroStageModal.tsx @@ -2,13 +2,16 @@ import { useTranslation } from 'react-i18next' import { Button, Dialog } from '@ensdomains/thorin' -import { intros } from '@app/transaction-flow/intro' -import { TransactionIntro } from '@app/transaction-flow/types' +import { + AnyTransactionIntro, + type TransactionIntro, + type TransactionIntroComponentName, +} from '@app/transaction/user/intro' import { TransactionDisplayItemSingle } from '@app/types' -import { DisplayItems } from '../DisplayItems' +import { DisplayItems } from '../../DisplayItems' -export const IntroStageModal = ({ +export const IntroStageModal = ({ transactions, onSuccess, currentStep, @@ -17,7 +20,7 @@ export const IntroStageModal = ({ title, trailingLabel, stepStatus, -}: TransactionIntro & { +}: TransactionIntro & { transactions: | { name: string @@ -53,13 +56,11 @@ export const IntroStageModal = ({ const txCount = transactions.length - const Content = intros[content.name] - return ( <> - + {txCount > 1 && ( void + sendTransaction: () => void + incrementTransaction: () => void + canEnableTransactionRequest: boolean + requestLoading: boolean + requestExists: boolean + transactionLoading: boolean + isTransactionRequestCachedData: boolean + requestErrorExists: boolean +} + +export const TransactionModalActionButton = ({ + status, + currentTransactionIndex, + transactionCount, + onDismiss, + sendTransaction, + incrementTransaction, + canEnableTransactionRequest, + requestLoading, + requestExists, + transactionLoading, + isTransactionRequestCachedData, + requestErrorExists, +}: TransactionModalActionButtonProps) => { + const { t } = useTranslation() + + if (status === 'success') { + const final = currentTransactionIndex + 1 === transactionCount + + if (final) { + return ( + + ) + } + return ( + + ) + } + if (status === 'reverted') { + return ( + + ) + } + if (status === 'pending') { + return ( + + ) + } + if (transactionLoading) { + return ( + + ) + } + return ( + + ) +} diff --git a/src/transaction/components/stage/transaction/BackButton.tsx b/src/transaction/components/stage/transaction/BackButton.tsx new file mode 100644 index 000000000..1dc4495a3 --- /dev/null +++ b/src/transaction/components/stage/transaction/BackButton.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from 'react-i18next' + +import { Button } from '@ensdomains/thorin' + +import type { StoredTransactionStatus } from '@app/transaction/slices/createTransactionSlice' +import { useTransactionManager } from '@app/transaction/transactionManager' + +export const BackButton = ({ + status, + backToInput, +}: { + status: StoredTransactionStatus + backToInput: boolean +}) => { + const { t } = useTranslation() + const setStage = useTransactionManager((s) => s.setCurrentFlowStage) + const resetTransactionIndex = useTransactionManager((s) => s.resetCurrentFlowTransactionIndex) + + if (!backToInput) return null + + if (status === 'waitingForUser' || status === 'pending' || status === 'success') return null + + const handleBackToInput = () => { + setStage('input') + resetTransactionIndex() + } + + return ( + + ) +} diff --git a/src/transaction/components/stage/transaction/LoadBar.tsx b/src/transaction/components/stage/transaction/LoadBar.tsx new file mode 100644 index 000000000..1e8c4563f --- /dev/null +++ b/src/transaction/components/stage/transaction/LoadBar.tsx @@ -0,0 +1,228 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' + +import { CrossCircleSVG, QuestionCircleSVG, Spinner, Typography } from '@ensdomains/thorin' + +import AeroplaneSVG from '@app/assets/Aeroplane.svg' +import CircleTickSVG from '@app/assets/CircleTick.svg' +import { Outlink } from '@app/components/Outlink' + +import type { DialogStatus } from './TransactionStageModal' + +const BarContainer = styled.div( + ({ theme }) => css` + width: ${theme.space.full}; + display: flex; + flex-direction: column; + align-items: center; + gap: ${theme.space['2']}; + `, +) + +const Bar = styled.div<{ $status: Exclude }>( + ({ theme, $status }) => css` + width: ${theme.space.full}; + height: ${theme.space['9']}; + border-radius: ${theme.radii.full}; + background-color: ${theme.colors.blueSurface}; + overflow: hidden; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + + --bar-color: ${theme.colors.blue}; + + ${$status === 'success' && + css` + --bar-color: ${theme.colors.green}; + `} + ${$status === 'reverted' && + css` + --bar-color: ${theme.colors.red}; + `} + `, +) + +const BarTypography = styled(Typography)( + ({ theme }) => css` + color: ${theme.colors.background}; + font-weight: ${theme.fontWeights.bold}; + `, +) + +const ProgressTypography = styled(Typography)( + ({ theme }) => css` + color: ${theme.colors.accent}; + font-weight: ${theme.fontWeights.bold}; + text-align: center; + `, +) + +const AeroplaneIcon = styled.svg( + ({ theme }) => css` + width: ${theme.space['4']}; + height: ${theme.space['4']}; + color: ${theme.colors.background}; + `, +) + +const CircleIcon = styled.svg( + ({ theme }) => css` + width: ${theme.space['6']}; + height: ${theme.space['6']}; + color: ${theme.colors.background}; + `, +) + +const MessageTypography = styled(Typography)( + () => css` + text-align: center; + `, +) + +const BarPrefix = styled.div( + ({ theme }) => css` + padding: ${theme.space['2']} ${theme.space['4']}; + width: min-content; + white-space: nowrap; + height: ${theme.space['9']}; + margin-right: -1px; + + border-radius: ${theme.radii.full}; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + background-color: var(--bar-color); + `, +) + +const InnerBar = styled.div( + ({ theme }) => css` + padding: ${theme.space['2']} ${theme.space['4']}; + height: ${theme.space['9']}; + + border-radius: ${theme.radii.full}; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + transition: width 1s linear; + &.progress-complete { + width: 100% !important; + padding-right: ${theme.space['2']}; + transition: width 0.5s ease-in-out; + } + + background-color: var(--bar-color); + + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + + position: relative; + + & > svg { + position: absolute; + right: ${theme.space['2']}; + top: 50%; + transform: translateY(-50%); + } + `, +) + +export const LoadBar = ({ + dialogStatus, + sendTime, +}: { + dialogStatus: Exclude + sendTime: number | undefined +}) => { + const { t } = useTranslation() + + const time = useMemo(() => ({ start: sendTime || Date.now(), ms: 45000 }), [sendTime]) + const [{ progress }, setProgress] = useState({ progress: 0, timeLeft: 45 }) + + const intervalFunc = useCallback( + (interval?: NodeJS.Timeout) => { + const timeElapsed = Date.now() - time.start + const _timeLeft = time.ms - timeElapsed + const _progress = Math.min((timeElapsed / (timeElapsed + _timeLeft)) * 100, 100) + setProgress({ timeLeft: Math.floor(_timeLeft / 1000), progress: _progress }) + if (_progress === 100) clearInterval(interval) + }, + [time.ms, time.start], + ) + + useEffect(() => { + intervalFunc() + const interval = setInterval(intervalFunc, 1000) + return () => clearInterval(interval) + }, [intervalFunc]) + + const message = useMemo(() => { + if (dialogStatus === 'success') { + return t('transaction.dialog.success.message') + } + if (dialogStatus === 'reverted') { + return null + } + return t('transaction.dialog.pending.message') + }, [dialogStatus, t]) + + const isTakingLongerThanExpected = dialogStatus === 'pending' && progress === 100 + + const progressMessage = useMemo(() => { + if (isTakingLongerThanExpected) { + return ( + + {t('transaction.dialog.sent.learn')} + + ) + } + return null + }, [isTakingLongerThanExpected, t]) + + const EndElement = useMemo(() => { + if (dialogStatus === 'success') { + return + } + if (dialogStatus === 'reverted') { + return + } + if (progress !== 100) { + return + } + return + }, [progress, dialogStatus]) + + return ( + <> + + + + + {t( + isTakingLongerThanExpected + ? 'transaction.dialog.pending.progress.message' + : `transaction.dialog.${dialogStatus}.progress.title`, + )} + + + + {EndElement} + + + {progressMessage && {progressMessage}} + + {message && {message}} + + ) +} diff --git a/src/transaction/components/stage/transaction/TransactionStageModal.tsx b/src/transaction/components/stage/transaction/TransactionStageModal.tsx new file mode 100644 index 000000000..c1b6d2c98 --- /dev/null +++ b/src/transaction/components/stage/transaction/TransactionStageModal.tsx @@ -0,0 +1,217 @@ +import { queryOptions } from '@tanstack/react-query' +import { useEffect, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { BaseError } from 'viem' + +import { Dialog, Helper, Typography } from '@ensdomains/thorin' + +import WalletSVG from '@app/assets/Wallet.svg' +import { Outlink } from '@app/components/Outlink' +import { useQueryOptions } from '@app/hooks/useQueryOptions' +import type { + GenericStoredTransaction, + StoredTransactionStatus, +} from '@app/transaction/slices/createTransactionSlice' +import { useTransactionManager } from '@app/transaction/transactionManager' +import type { UserTransactionName } from '@app/transaction/user/transaction' +import { TransactionDisplayItem } from '@app/types' +import { getReadableError } from '@app/utils/errors' +import { useQuery } from '@app/utils/query/useQuery' +import { createEtherscanLink } from '@app/utils/utils' + +import { DisplayItems } from '../../DisplayItems' +import { TransactionModalActionButton } from './ActionButton' +import { BackButton } from './BackButton' +import { LoadBar } from './LoadBar' +import { getTransactionErrorQueryFn } from './query' +import { useManagedTransaction } from './useManagedTransaction' + +const WalletIcon = styled.svg( + ({ theme }) => css` + width: ${theme.space['12']}; + `, +) + +const MessageTypography = styled(Typography)( + () => css` + text-align: center; + `, +) + +function useCreateSubnameRedirect( + shouldTrigger: boolean, + subdomain?: TransactionDisplayItem['value'], +) { + useEffect(() => { + if (shouldTrigger && typeof subdomain === 'string') { + setTimeout(() => { + window.location.href = `/${subdomain}` + }, 1000) + } + }, [shouldTrigger, subdomain]) +} + +type TransactionStageModalProps = { + currentTransactionIndex: number + transactionCount: number + transaction: GenericStoredTransaction + displayItems: TransactionDisplayItem[] + backToInput: boolean + onDismiss: () => void +} + +export type DialogStatus = Exclude | 'confirm' + +const MiddleContent = ({ + dialogStatus, + sendTime, +}: { + dialogStatus: DialogStatus + sendTime: number | undefined +}) => { + const { t } = useTranslation() + + if (dialogStatus !== 'confirm') return + + return ( + <> + + {t('transaction.dialog.confirm.message')} + + ) +} + +export const TransactionStageModal = ({ + currentTransactionIndex, + transactionCount, + transaction, + displayItems, + backToInput, + onDismiss, +}: TransactionStageModalProps) => { + const { t } = useTranslation() + + const incrementTransaction = useTransactionManager((s) => s.incrementCurrentFlowTransactionIndex) + + const { + transactionError, + requestError, + canEnableTransactionRequest, + isTransactionRequestCachedData, + request, + requestLoading, + sendTransaction, + transactionLoading, + } = useManagedTransaction(transaction) + + useCreateSubnameRedirect( + transaction.status === 'success' && currentTransactionIndex + 1 === transactionCount, + displayItems.find((i) => i.label === 'subname' && i.type === 'name')?.value, + ) + + const FilledDisplayItems = useMemo( + () => , + [displayItems], + ) + + const stepStatus = useMemo(() => { + if (transaction.status === 'success') { + return 'completed' + } + return 'inProgress' + }, [transaction.status]) + + const initialErrorOptions = useQueryOptions({ + params: { + hash: transaction.currentHash, + status: transaction.status, + targetChainId: transaction.targetChainId, + }, + functionName: 'getTransactionError', + queryDependencyType: 'standard', + queryFn: getTransactionErrorQueryFn, + }) + + const preparedErrorOptions = queryOptions({ + queryKey: initialErrorOptions.queryKey, + queryFn: initialErrorOptions.queryFn, + }) + + const { data: upperError } = useQuery({ + ...preparedErrorOptions, + enabled: !!transaction && !!transaction.currentHash && transaction.status === 'reverted', + }) + + const lowerError = useMemo(() => { + if (transaction.status === 'success') return null + if (transaction.status === 'pending') return null + if (transaction.status === 'waitingForUser') return null + const err = transactionError || requestError + if (!err) return null + if (!(err instanceof BaseError)) { + if ('message' in err) return err.message + return t('transaction.error.unknown') + } + const readableError = getReadableError(err) + return readableError || err.shortMessage + }, [t, transaction.status, transactionError, requestError]) + + const actionButton = ( + sendTransaction(request!)} + status={transaction.status} + transactionLoading={transactionLoading} + /> + ) + + const backButton = + + const dialogStatus = (() => { + switch (transaction.status) { + case 'empty': + case 'waitingForUser': + return 'confirm' + default: + return transaction.status + } + })() + + return ( + <> + + + + {upperError && {t(upperError)}} + {FilledDisplayItems} + {transaction.currentHash && ( + + {t('transaction.viewEtherscan')} + + )} + {lowerError && {lowerError}} + + 1 ? transactionCount : undefined} + stepStatus={stepStatus} + leading={backButton} + trailing={actionButton} + /> + + ) +} diff --git a/src/components/@molecules/TransactionDialogManager/stage/query.ts b/src/transaction/components/stage/transaction/query.ts similarity index 54% rename from src/components/@molecules/TransactionDialogManager/stage/query.ts rename to src/transaction/components/stage/transaction/query.ts index 25a0910a8..1c88fc147 100644 --- a/src/components/@molecules/TransactionDialogManager/stage/query.ts +++ b/src/transaction/components/stage/transaction/query.ts @@ -1,29 +1,21 @@ import { QueryFunctionContext } from '@tanstack/react-query' import { CallParameters, SendTransactionReturnType } from '@wagmi/core' -import { Dispatch } from 'react' -import { - Address, - BlockTag, - Hash, - Hex, - PrepareTransactionRequestRequest, - toHex, - Transaction, - TransactionRequest, -} from 'viem' +import { Address, BlockTag, Hash, Hex, toHex, TransactionRequest } from 'viem' import { call, estimateGas, getTransaction, prepareTransactionRequest } from 'viem/actions' - -import { SupportedChain } from '@app/constants/chains' -import { TransactionStatus } from '@app/hooks/transactions/transactionStore' -import { useAddRecentTransaction } from '@app/hooks/transactions/useAddRecentTransaction' -import { useIsSafeApp } from '@app/hooks/useIsSafeApp' -import { createTransactionRequest, TransactionName } from '@app/transaction-flow/transaction' +import type { SendTransactionVariables } from 'wagmi/query' + +import { SupportedChain, type TargetChain } from '@app/constants/chains' +import type { + GenericStoredTransaction, + StoredTransactionIdentifiers, + StoredTransactionStatus, +} from '@app/transaction/slices/createTransactionSlice' +import { useTransactionManager } from '@app/transaction/transactionManager' import { - GetUniqueTransactionParameters, - ManagedDialogProps, - TransactionFlowAction, - UniqueTransaction, -} from '@app/transaction-flow/types' + createTransactionRequest, + type UserTransactionData, + type UserTransactionName, +} from '@app/transaction/user/transaction' import { BasicTransactionRequest, ClientWithEns, @@ -32,6 +24,7 @@ import { CreateQueryKey, } from '@app/types' import { getReadableError } from '@app/utils/errors' +import type { wagmiConfig } from '@app/utils/query/wagmi' import { CheckIsSafeAppReturnType } from '@app/utils/safe' type AccessListResponse = { @@ -42,75 +35,48 @@ type AccessListResponse = { gasUsed: Hex } -export const getUniqueTransaction = ({ - txKey, - currentStep, - transaction, -}: GetUniqueTransactionParameters): UniqueTransaction => ({ - key: txKey!, - step: currentStep, - name: transaction.name, - data: transaction.data, -}) +type TransactionIdentifiersWithData = + StoredTransactionIdentifiers & { + name: name + data: UserTransactionData + } -export const transactionSuccessHandler = +export const getTransactionIdentifiersWithData = < + name extends UserTransactionName = UserTransactionName, +>( + transaction: GenericStoredTransaction, +): TransactionIdentifiersWithData => { + const { sourceChainId, targetChainId, account, transactionId, flowId, name, data } = transaction + return { sourceChainId, targetChainId, account, transactionId, flowId, name, data } +} + +export const transactionMutateHandler = ({ - client, - connectorClient, - actionName, - txKey, - request, - addRecentTransaction, - dispatch, + transactionIdentifiers, isSafeApp, }: { - client: ClientWithEns - connectorClient: ConnectorClientWithEns - actionName: ManagedDialogProps['actionName'] - txKey: string | null - request: PrepareTransactionRequestRequest | undefined - addRecentTransaction: ReturnType - dispatch: Dispatch - isSafeApp: ReturnType['data'] + transactionIdentifiers: StoredTransactionIdentifiers + isSafeApp: CheckIsSafeAppReturnType }) => - async (tx: SendTransactionReturnType) => { - let transactionData: Transaction | null = null - try { - // If using private mempool, this won't error, will return null - transactionData = await connectorClient.request<{ - Method: 'eth_getTransactionByHash' - Parameters: [hash: Hash] - ReturnType: Transaction | null - }>({ method: 'eth_getTransactionByHash', params: [tx] }) - } catch (e) { - // this is expected to fail in most cases - } - - if (!transactionData) { - try { - transactionData = await client.request({ - method: 'eth_getTransactionByHash', - params: [tx], - }) - } catch (e) { - console.error('Failed to get transaction info') - } - } - - addRecentTransaction({ - ...transactionData, - hash: tx, - action: actionName, - key: txKey!, - input: request?.data, - timestamp: Math.floor(Date.now() / 1000), - isSafeTx: !!isSafeApp, - searchRetries: 0, + (request: SendTransactionVariables) => { + useTransactionManager.getState().setTransactionSubmission(transactionIdentifiers, { + input: request.data!, + nonce: request.nonce!, + timestamp: Date.now(), + transactionType: isSafeApp ? 'safe' : 'standard', }) - dispatch({ name: 'setTransactionHash', payload: { hash: tx, key: txKey! } }) } -export const registrationGasFeeModifier = (gasLimit: bigint, transactionName: TransactionName) => +export const transactionSuccessHandler = + (transactionIdentifiers: StoredTransactionIdentifiers) => + async (transactionHash: SendTransactionReturnType) => { + useTransactionManager.getState().setTransactionHash(transactionIdentifiers, transactionHash) + } + +export const registrationGasFeeModifier = ( + gasLimit: bigint, + transactionName: UserTransactionName, +) => // this addition is arbitrary, something to do with a gas refund but not 100% sure transactionName === 'registerName' ? gasLimit + 5000n : gasLimit @@ -125,7 +91,7 @@ export const calculateGasLimit = async ({ connectorClient: ConnectorClientWithEns isSafeApp: boolean txWithZeroGas: BasicTransactionRequest - transactionName: TransactionName + transactionName: UserTransactionName }) => { if (isSafeApp) { const accessListResponse = await client.request<{ @@ -161,8 +127,8 @@ export const calculateGasLimit = async ({ } } -type CreateTransactionRequestQueryKey = CreateQueryKey< - UniqueTransaction, +type CreateTransactionRequestQueryKey = CreateQueryKey< + TransactionIdentifiersWithData, 'createTransactionRequest', 'standard' > @@ -176,13 +142,13 @@ export const createTransactionRequestQueryFn = connectorClient: ConnectorClientWithEns | undefined isSafeApp: CheckIsSafeAppReturnType | undefined }) => - async ({ - queryKey: [params, chainId, address], - }: QueryFunctionContext) => { - const client = config.getClient({ chainId }) + async ({ + queryKey: [params], + }: QueryFunctionContext>) => { + const client = config.getClient({ chainId: params.targetChainId }) if (!connectorClient) throw new Error('connectorClient is required') - if (connectorClient.account.address !== address) + if (connectorClient.account.address !== params.account) throw new Error('address does not match connector') const transactionRequest = await createTransactionRequest({ @@ -206,13 +172,16 @@ export const createTransactionRequestQueryFn = transactionName: params.name, }) + const prepareParameters = + params.name === '__dev_failure' ? [] : (['fees', 'nonce', 'type'] as const) + const request = await prepareTransactionRequest(client, { to: transactionRequest.to, accessList, account: connectorClient.account, data: transactionRequest.data, gas: gasLimit, - parameters: ['fees', 'nonce', 'type'], + parameters: prepareParameters, ...('value' in transactionRequest ? { value: transactionRequest.value } : {}), }) @@ -221,12 +190,18 @@ export const createTransactionRequestQueryFn = chain: request.chain!, to: request.to!, gas: request.gas!, - chainId, + chainId: params.targetChainId, } } +type GetTransactionErrorParameters = { + hash: Hash | null + status: StoredTransactionStatus | undefined + targetChainId: TargetChain['id'] +} + type GetTransactionErrorQueryKey = CreateQueryKey< - { hash: Hash | undefined; status: Exclude | undefined }, + GetTransactionErrorParameters, 'getTransactionError', 'standard' > @@ -234,10 +209,10 @@ type GetTransactionErrorQueryKey = CreateQueryKey< export const getTransactionErrorQueryFn = (config: ConfigWithEns) => async ({ - queryKey: [{ hash, status }, chainId], + queryKey: [{ hash, status, targetChainId }], }: QueryFunctionContext) => { - if (!hash || status !== 'failed') return null - const client = config.getClient({ chainId }) + if (!hash || status !== 'reverted') return null + const client = config.getClient({ chainId: targetChainId }) const failedTransactionData = await getTransaction(client, { hash }) try { await call(client, failedTransactionData as CallParameters) diff --git a/src/transaction/components/stage/transaction/useManagedTransaction.ts b/src/transaction/components/stage/transaction/useManagedTransaction.ts new file mode 100644 index 000000000..b8a9c517e --- /dev/null +++ b/src/transaction/components/stage/transaction/useManagedTransaction.ts @@ -0,0 +1,95 @@ +import { queryOptions, useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' +import { useConnectorClient, useSendTransaction } from 'wagmi' + +import { useInvalidateOnBlock } from '@app/hooks/chain/useInvalidateOnBlock' +import { useIsSafeApp } from '@app/hooks/useIsSafeApp' +import { useQueryOptions } from '@app/hooks/useQueryOptions' +import type { GenericStoredTransaction } from '@app/transaction/slices/createTransactionSlice' +import type { UserTransactionName } from '@app/transaction/user/transaction' +import type { ConfigWithEns } from '@app/types' +import { getIsCachedData } from '@app/utils/getIsCachedData' + +import { + createTransactionRequestQueryFn, + getTransactionIdentifiersWithData, + transactionMutateHandler, + transactionSuccessHandler, +} from './query' + +export const useManagedTransaction = ( + transaction: GenericStoredTransaction, +) => { + const { data: isSafeApp, isLoading: safeAppStatusLoading } = useIsSafeApp() + const { data: connectorClient } = useConnectorClient() + + const transactionIdentifiers = useMemo( + () => getTransactionIdentifiersWithData(transaction), + [transaction], + ) + + // if not all unique identifiers are defined, there could be incorrect cached data + const isUniquenessDefined = useMemo( + // number check is for if step = 0 + () => Object.values(transactionIdentifiers).every((val) => typeof val === 'number' || !!val), + [transactionIdentifiers], + ) + + const canEnableTransactionRequest = useMemo( + () => + !!transaction && + !!connectorClient?.account && + !safeAppStatusLoading && + !(transaction.status === 'pending' || transaction.status === 'success') && + isUniquenessDefined, + [transaction, connectorClient?.account, safeAppStatusLoading, isUniquenessDefined], + ) + + const initialOptions = useQueryOptions({ + params: transactionIdentifiers, + functionName: 'createTransactionRequest', + queryDependencyType: 'standard', + queryFn: createTransactionRequestQueryFn, + }) + + const preparedOptions = queryOptions({ + queryKey: initialOptions.queryKey, + queryFn: initialOptions.queryFn({ connectorClient, isSafeApp }), + }) + + const transactionRequestQuery = useQuery({ + ...preparedOptions, + enabled: canEnableTransactionRequest, + refetchOnMount: 'always', + }) + + const { data: request, isLoading: requestLoading, error: requestError } = transactionRequestQuery + const isTransactionRequestCachedData = getIsCachedData(transactionRequestQuery) + + useInvalidateOnBlock({ + enabled: canEnableTransactionRequest && process.env.NEXT_PUBLIC_ETH_NODE !== 'anvil', + queryKey: preparedOptions.queryKey, + }) + + const { + isPending: transactionLoading, + error: transactionError, + sendTransaction, + } = useSendTransaction({ + mutation: { + onMutate: transactionMutateHandler({ transactionIdentifiers, isSafeApp: isSafeApp! }), + onSuccess: transactionSuccessHandler(transactionIdentifiers), + }, + }) + + return { + request, + requestLoading, + requestError, + isTransactionRequestCachedData, + canEnableTransactionRequest, + transactionLoading, + transactionError, + sendTransaction, + } +} diff --git a/src/transaction/key.ts b/src/transaction/key.ts new file mode 100644 index 000000000..797a2e1e4 --- /dev/null +++ b/src/transaction/key.ts @@ -0,0 +1,16 @@ +import type { FlowKey, StoredFlow } from './slices/createFlowSlice' +import type { StoredTransaction, TransactionKey } from './slices/createTransactionSlice' + +export const getFlowKey = ( + flow: Pick, +): FlowKey => JSON.stringify([flow.flowId, flow.sourceChainId, flow.account]) as FlowKey +export const getTransactionKey = ({ + transactionId, + flowId, + sourceChainId, + account, +}: Pick< + StoredTransaction, + 'transactionId' | 'flowId' | 'sourceChainId' | 'account' +>): TransactionKey => + JSON.stringify([transactionId, flowId, sourceChainId, account]) as TransactionKey diff --git a/src/transaction/listeners/createTransactionListener.ts b/src/transaction/listeners/createTransactionListener.ts new file mode 100644 index 000000000..ca6f2bcbc --- /dev/null +++ b/src/transaction/listeners/createTransactionListener.ts @@ -0,0 +1,21 @@ +import type { AllSlices } from '../slices/types' + +export type TransactionStoreListener = [ + selector: (state: AllSlices) => selected, + listener: (selectedState: selected, previousSelectedState: selected) => void, + options?: { + equalityFn?: (a: selected, b: selected) => boolean + fireImmediately?: boolean + }, +] + +export const createTransactionListener = ( + selector: (state: AllSlices) => selected, + listener: (selectedState: selected, previousSelectedState: selected) => void, + options?: { + equalityFn?: (a: selected, b: selected) => boolean + fireImmediately?: boolean + }, +): TransactionStoreListener => { + return [selector, listener, options] +} diff --git a/src/transaction/listeners/existingCommitListener.ts b/src/transaction/listeners/existingCommitListener.ts new file mode 100644 index 000000000..c0c46ef0a --- /dev/null +++ b/src/transaction/listeners/existingCommitListener.ts @@ -0,0 +1,254 @@ +import { decodeFunctionData, encodeFunctionData, getAddress, toFunctionSelector } from 'viem' +import { getBlock, readContract } from 'viem/actions' + +import { + ethRegistrarControllerCommitmentsSnippet, + ethRegistrarControllerCommitSnippet, + getChainContractAddress, +} from '@ensdomains/ensjs/contracts' +import { makeCommitment } from '@ensdomains/ensjs/utils' + +import type { ClientWithEns } from '@app/types' +import { wagmiConfig } from '@app/utils/query/wagmi' + +import { getTransactionKey } from '../key' +import type { StoredTransactionResult } from '../slices/createTransactionSlice' +import type { UseTransactionManager } from '../transactionManager' +import { createTransactionListener } from './createTransactionListener' +import { getBlockMetadataByTimestamp } from './utils/getBlockMetadataByTimestamp' + +const commitSearchCache = new Map>() + +type SearchableCommitTransaction = Extract< + StoredTransactionResult<'empty' | 'pending' | 'waitingForUser'>, + { name: 'commitName' } +> + +const maxCommitmentAgeSnippet = [ + { + inputs: [], + name: 'maxCommitmentAge', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const + +const getCurrentBlockTimestampSnippet = [ + { + inputs: [], + name: 'getCurrentBlockTimestamp', + outputs: [ + { + internalType: 'uint256', + name: 'timestamp', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const + +const execTransactionSnippet = [ + { + inputs: [ + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + { internalType: 'bytes', name: 'data', type: 'bytes' }, + { + internalType: 'enum Enum.Operation', + name: 'operation', + type: 'uint8', + }, + { internalType: 'uint256', name: 'safeTxGas', type: 'uint256' }, + { internalType: 'uint256', name: 'baseGas', type: 'uint256' }, + { internalType: 'uint256', name: 'gasPrice', type: 'uint256' }, + { internalType: 'address', name: 'gasToken', type: 'address' }, + { + internalType: 'address payable', + name: 'refundReceiver', + type: 'address', + }, + { internalType: 'bytes', name: 'signatures', type: 'bytes' }, + ], + name: 'execTransaction', + outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }], + stateMutability: 'payable', + type: 'function', + }, +] as const + +const attemptCommitSearch = async ( + client: ClientWithEns, + storedTransaction: SearchableCommitTransaction, +) => { + const { data: commitmentData, transactionType, account } = storedTransaction + const commitment = makeCommitment(commitmentData) + + const ethRegistrarControllerAddress = getChainContractAddress({ + client, + contract: 'ensEthRegistrarController', + }) + const multicall3Address = getChainContractAddress({ + client, + contract: 'multicall3', + }) + + const [commitmentTimestamp, maxCommitmentAge, blockTimestamp] = await Promise.all([ + readContract(client, { + abi: ethRegistrarControllerCommitmentsSnippet, + address: ethRegistrarControllerAddress, + functionName: 'commitments', + args: [commitment], + }), + readContract(client, { + abi: maxCommitmentAgeSnippet, + address: ethRegistrarControllerAddress, + functionName: 'maxCommitmentAge', + }), + readContract(client, { + abi: getCurrentBlockTimestampSnippet, + address: multicall3Address, + functionName: 'getCurrentBlockTimestamp', + }), + ]) + + if (!commitmentTimestamp || commitmentTimestamp === 0n) + return { + status: 'notFound', + currentHash: null, + timestamp: 0, + } as const + + const commitmentAge = blockTimestamp - commitmentTimestamp + const commitmentTimestampNumber = Number(commitmentTimestamp) + + if (commitmentAge > maxCommitmentAge) + return { + status: 'commitmentExpired', + currentHash: null, + timestamp: commitmentTimestampNumber * 1000, + } as const + + const existsEarlyEscape = () => + ({ + status: 'commitmentExists', + currentHash: null, + timestamp: commitmentTimestampNumber * 1000, + }) as const + + const blockMetadata = await getBlockMetadataByTimestamp(client, { + timestamp: commitmentTimestamp, + }) + if (!blockMetadata.ok) return existsEarlyEscape() + + const blockData = await getBlock(client, { + blockHash: blockMetadata.data.hash, + includeTransactions: true, + }).catch(() => null) + if (!blockData) return existsEarlyEscape() + + const inputData = encodeFunctionData({ + abi: ethRegistrarControllerCommitSnippet, + args: [commitment], + functionName: 'commit', + }) + + const transaction = (() => { + const checksummedAddress = getAddress(account) + const checksummedEthRegistrarControllerAddress = getAddress(ethRegistrarControllerAddress) + if (transactionType === 'safe') { + const execTransactionFunctionSelector = toFunctionSelector(execTransactionSnippet[0]) + const foundTransaction = blockData.transactions.find((t) => { + // safe transaction gets sent to the safe contract itself + if (!t.to || getAddress(t.to) !== checksummedAddress) return false + if (!t.input.startsWith(execTransactionFunctionSelector)) return false + const { args: safeTxData } = decodeFunctionData({ + abi: execTransactionSnippet, + data: t.input, + }) + if (getAddress(safeTxData[0]) !== checksummedEthRegistrarControllerAddress) return false + if (getAddress(safeTxData[2]) !== inputData) return false + return true + }) + return foundTransaction + } + const foundTransaction = blockData.transactions.find((t) => { + if (getAddress(t.from) !== checksummedAddress) return false + if (!t.to || getAddress(t.to) !== checksummedEthRegistrarControllerAddress) return false + if (t.input !== inputData) return false + return true + }) + return foundTransaction + })() + + if (!transaction) return existsEarlyEscape() + + return { + status: 'commitmentFound', + currentHash: transaction.hash, + timestamp: commitmentTimestampNumber * 1000, + } as const +} + +const listenForExistingCommit = async ( + store: UseTransactionManager, + transaction: SearchableCommitTransaction, +) => { + const client = wagmiConfig.getClient({ chainId: transaction.targetChainId }) + + const cleanup = () => { + commitSearchCache.delete(getTransactionKey(transaction)) + } + + for (;;) { + /* eslint-disable no-await-in-loop */ + const result = await attemptCommitSearch(client, transaction) + const state = store.getState() + // ensure transaction wasn't already found + if (state.getTransaction(transaction)?.status === 'success') return cleanup() + + switch (result.status) { + case 'commitmentFound': + state.setTransactionHash(transaction, result.currentHash) + // eslint-disable-next-line no-fallthrough + case 'commitmentExpired': + case 'commitmentExists': + state.setTransactionStatus(transaction, 'success') + state.setTransactionReceipt(transaction, { timestamp: result.timestamp }) + return cleanup() + default: + await new Promise((resolve) => { + setTimeout(resolve, 10_000) + }) + break + } + /* eslint-enable no-await-in-loop */ + } +} + +export const existingCommitListener = (store: UseTransactionManager) => + createTransactionListener( + (s) => + s + .getTransactionsByStatus(['empty', 'pending', 'waitingForUser']) + .filter((t): t is SearchableCommitTransaction => t.name === 'commitName'), + (applicableTransactions) => { + for (const tx of applicableTransactions) { + const transactionKey = getTransactionKey(tx) + const existingSearchPromise = commitSearchCache.get(transactionKey) + // eslint-disable-next-line no-continue + if (existingSearchPromise) continue + + const searchPromise = listenForExistingCommit(store, tx) + commitSearchCache.set(transactionKey, searchPromise) + } + }, + ) diff --git a/src/transaction/listeners/transactionReceiptListener.ts b/src/transaction/listeners/transactionReceiptListener.ts new file mode 100644 index 000000000..487df9bcb --- /dev/null +++ b/src/transaction/listeners/transactionReceiptListener.ts @@ -0,0 +1,60 @@ +import type { Block, Hash } from 'viem' +import { getBlock } from 'viem/actions' + +import { waitForTransaction } from '@app/hooks/transactions/waitForTransaction' +import { wagmiConfig } from '@app/utils/query/wagmi' + +import { getTransactionKey } from '../key' +import type { StoredTransactionList } from '../slices/createTransactionSlice' +import type { UseTransactionManager } from '../transactionManager' +import { createTransactionListener } from './createTransactionListener' + +const transactionRequestCache = new Map>() +const blockRequestCache = new Map>() + +const listenForTransaction = async ( + store: UseTransactionManager, + transaction: StoredTransactionList<'pending'>[number], +) => { + const receipt = await waitForTransaction(wagmiConfig, { + confirmations: 1, + hash: transaction.currentHash, + isSafeTx: transaction.transactionType === 'safe', + chainId: transaction.targetChainId, + onReplaced: (replacedTransaction) => { + if (replacedTransaction.reason !== 'repriced') return + store.getState().setTransactionHash(transaction, replacedTransaction.transaction.hash) + }, + }) + + const { status, blockHash } = receipt + let blockRequest = blockRequestCache.get(blockHash) + if (!blockRequest) { + const client = wagmiConfig.getClient({ chainId: transaction.targetChainId }) + blockRequest = getBlock(client, { blockHash }) + blockRequestCache.set(blockHash, blockRequest) + } + + const { timestamp } = await blockRequest + store.getState().setTransactionReceipt(transaction, { timestamp: Number(timestamp) * 1000 }) + store.getState().setTransactionStatus(transaction, status) + + const transactionKey = getTransactionKey(transaction) + transactionRequestCache.delete(transactionKey) +} + +export const transactionReceiptListener = (store: UseTransactionManager) => + createTransactionListener( + (s) => s.getTransactionsByStatus('pending'), + (pendingTransactions) => { + for (const tx of pendingTransactions) { + const transactionKey = getTransactionKey(tx) + const existingRequest = transactionRequestCache.get(transactionKey) + // eslint-disable-next-line no-continue + if (existingRequest) continue + + const requestPromise = listenForTransaction(store, tx) + transactionRequestCache.set(transactionKey, requestPromise) + } + }, + ) diff --git a/src/hooks/registration/utils/getBlockMetadataByTimestamp.ts b/src/transaction/listeners/utils/getBlockMetadataByTimestamp.ts similarity index 100% rename from src/hooks/registration/utils/getBlockMetadataByTimestamp.ts rename to src/transaction/listeners/utils/getBlockMetadataByTimestamp.ts diff --git a/src/transaction/slices/createCurrentSlice.ts b/src/transaction/slices/createCurrentSlice.ts new file mode 100644 index 000000000..426f60f85 --- /dev/null +++ b/src/transaction/slices/createCurrentSlice.ts @@ -0,0 +1,69 @@ +/* eslint-disable no-param-reassign */ +import { getAccount, getChainId, watchChainId } from '@wagmi/core' +import type { Address } from 'viem' +import type { StateCreator } from 'zustand' + +import type { SourceChain, SupportedChain } from '@app/constants/chains' +import { getSourceChainId } from '@app/utils/query/getSourceChainId' +import { wagmiConfig } from '@app/utils/query/wagmi' + +import type { FlowId } from './createFlowSlice' +import type { AllSlices, MiddlewareArray } from './types' + +export type CurrentSlice = { + current: { + sourceChainId: SourceChain['id'] | null + account: Address | null + flowId: FlowId | null + _previousFlowId: FlowId | null + } + onChainIdUpdate: (chainId: SupportedChain['id']) => void + onAccountUpdate: (account: Address | undefined) => void + _hasHydrated: boolean + _setHasHydrated: (hasHydrated: boolean) => void +} + +export const createCurrentSlice: StateCreator = ( + set, +) => { + const onChainIdUpdate = (chainId: SupportedChain['id']) => + set((state) => { + const oldSourceChainId = state.current.sourceChainId + const newSourceChainId = getSourceChainId(chainId) + if (oldSourceChainId !== newSourceChainId) { + state.current.sourceChainId = newSourceChainId + state.current.flowId = null + state.clearNotifications() + } + }) + + const onAccountUpdate = (account: Address | undefined) => + set((state) => { + state.current.account = account ?? null + state.current.flowId = null + state.clearNotifications() + }) + + wagmiConfig.subscribe(() => getAccount(wagmiConfig).address, onAccountUpdate) + + watchChainId(wagmiConfig, { + onChange: onChainIdUpdate, + }) + return { + current: { + sourceChainId: null, + account: null, + flowId: null, + _previousFlowId: null, + }, + onChainIdUpdate, + onAccountUpdate, + _hasHydrated: false, + _setHasHydrated: (hasHydrated) => + set((state) => { + state._hasHydrated = hasHydrated + onChainIdUpdate(getChainId(wagmiConfig)) + onAccountUpdate(getAccount(wagmiConfig).address) + }), + } +} diff --git a/src/transaction/slices/createFlowSlice.ts b/src/transaction/slices/createFlowSlice.ts new file mode 100644 index 000000000..1311f3f20 --- /dev/null +++ b/src/transaction/slices/createFlowSlice.ts @@ -0,0 +1,376 @@ +/* eslint-disable no-param-reassign */ + +import type { WritableDraft } from 'immer/dist/internal' +import type { Address } from 'viem' +import type { StateCreator } from 'zustand' + +import type { SourceChain } from '@app/constants/chains' + +import { getFlowKey, getTransactionKey } from '../key' +import type { TransactionStoreIdentifiers } from '../types' +import type { GenericTransactionInput, TransactionInput } from '../user/input' +import type { TransactionIntro } from '../user/intro' +import type { UserTransaction } from '../user/transaction' +import type { + StoredTransaction, + StoredTransactionList, + TransactionId, +} from './createTransactionSlice' +import type { AllSlices, MiddlewareArray } from './types' +import { getIdentifiers } from './utils' + +export type FlowId = string +export type FlowKey = `["${FlowId}",${SourceChain['id']},"${Address}"]` +export type TransactionFlowStage = 'input' | 'intro' | 'transaction' + +export type StoredFlow = TransactionStoreIdentifiers & { + flowId: FlowId + transactionIds: TransactionId[] + currentTransactionIndex: number + currentStage: TransactionFlowStage + input?: TransactionInput + intro?: TransactionIntro + resumable?: boolean + requiresManualCleanup?: boolean + autoClose?: boolean + resumeLink?: string + disableBackgroundClick?: boolean +} + +export type FlowInitialiserData = Omit< + StoredFlow, + 'currentStage' | 'currentTransactionIndex' | 'transactionIds' | keyof TransactionStoreIdentifiers +> & { + transactions: UserTransaction[] +} + +export type FlowSlice = { + flows: Map + + /* ID-specific Flow */ + /* Getters */ + getFlowOrNull: ( + flowId: FlowId, + identifiersOverride?: TransactionStoreIdentifiers, + ) => StoredFlow | null + getFlowStageOrNull: ( + flowId: FlowId, + identifiersOverride?: TransactionStoreIdentifiers, + ) => TransactionFlowStage | 'complete' | null + getFlowTransactions: ( + flowId: FlowId, + identifiersOverride?: TransactionStoreIdentifiers, + ) => StoredTransactionList + getFlowTransactionsOrNull: ( + flowId: FlowId, + identifiersOverride?: TransactionStoreIdentifiers, + ) => StoredTransactionList | null + isFlowResumable: (flowId: FlowId, identifiersOverride?: TransactionStoreIdentifiers) => boolean + /* Setters */ + showFlowInput: ( + flowId: FlowId, + { + input, + disableBackgroundClick, + }: { input: GenericTransactionInput; disableBackgroundClick?: boolean }, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + startFlow: (flow: FlowInitialiserData, identifiersOverride?: TransactionStoreIdentifiers) => void + resumeFlow: (flowId: FlowId, identifiersOverride?: TransactionStoreIdentifiers) => void + resumeFlowWithCheck: ( + flowId: FlowId, + { push }: { push: (path: string) => void }, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + cleanupFlow: (flowId: FlowId, identifiersOverride?: TransactionStoreIdentifiers) => void + cleanupFlowUnsafe: (flowId: FlowId, identifiersOverride?: TransactionStoreIdentifiers) => void + setFlowTransactions: ( + flowId: FlowId, + transactions: UserTransaction[], + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + + /* Current Flow */ + /* Getters */ + getCurrentFlowTransactions: () => StoredTransactionList + getCurrentOrPreviousFlow: () => { flow: StoredFlow | null; isPrevious: boolean } + /* Setters */ + setCurrentFlowTransactions: (transactions: UserTransaction[]) => void + setCurrentFlowStage: (stage: TransactionFlowStage) => void + stopCurrentFlow: () => void + attemptCurrentFlowDismiss: () => void + incrementCurrentFlowTransactionIndex: () => void + resetCurrentFlowTransactionIndex: () => void +} + +const getCurrentFlow = (state: AllSlices) => { + const { account, sourceChainId, flowId } = state.current + if (!flowId) throw new Error('No flowId found') + if (!account) throw new Error('No account found') + if (!sourceChainId) throw new Error('No sourceChainId found') + const flowKey = getFlowKey({ flowId, sourceChainId, account }) + const flow = state.flows.get(flowKey) + if (!flow) throw new Error('No flow found') + return flow +} +export const getCurrentFlowOrNull = (state: AllSlices) => { + const { account, sourceChainId, flowId } = state.current + if (!account || !sourceChainId || !flowId) return null + const flowKey = getFlowKey({ flowId, sourceChainId, account }) + return state.flows.get(flowKey) ?? null +} + +export const getAllTransactionsComplete = (state: AllSlices, flow: StoredFlow) => { + const identifiers = { + account: flow.account, + sourceChainId: flow.sourceChainId, + flowId: flow.flowId, + } + return flow.transactionIds.every((transactionId) => { + const transactionKey = getTransactionKey({ transactionId, ...identifiers }) + const transaction = state.transactions.get(transactionKey) + return transaction?.status === 'success' + }) +} + +export const getNoTransactionsStarted = (state: AllSlices, flow: StoredFlow) => { + const identifiers = { + account: flow.account, + sourceChainId: flow.sourceChainId, + flowId: flow.flowId, + } + return flow.transactionIds.every((transactionId) => { + const transactionKey = getTransactionKey({ transactionId, ...identifiers }) + const transaction = state.transactions.get(transactionKey) + return transaction?.status === 'empty' + }) +} + +export const getCanRemoveFlow = (state: AllSlices, flow: StoredFlow) => { + if (flow.requiresManualCleanup) return false + if (!flow.transactionIds || flow.transactionIds.length === 0) return true + if (!flow.resumable) return true + + if (getAllTransactionsComplete(state, flow)) return true + return getNoTransactionsStarted(state, flow) +} + +export const createFlowSlice: StateCreator = ( + set, + get, +) => ({ + flows: new Map(), + getFlowOrNull: (flowId, identifiersOverride) => { + const state = get() + const identifiers = getIdentifiers(state, identifiersOverride) + const flowKey = getFlowKey({ flowId, ...identifiers }) + return state.flows.get(flowKey) ?? null + }, + getFlowStageOrNull: (flowId, identifiersOverride) => { + const state = get() + const flow = state.getFlowOrNull(flowId, identifiersOverride) + if (!flow) return null + if (flow.currentStage !== 'transaction') return flow.currentStage + const transactions = state.getFlowTransactions(flow.flowId, flow) + if (transactions.length === 0) return 'complete' + const lastTransaction = transactions[transactions.length - 1] + if (lastTransaction.status === 'success') return 'complete' + return 'transaction' + }, + getFlowTransactionsOrNull: (flowId, identifiersOverride) => { + const state = get() + const flow = state.getFlowOrNull(flowId, identifiersOverride) + if (!flow) return null + return state.getFlowTransactions(flow.flowId, flow) + }, + getFlowTransactions: (flowId, identifiersOverride) => { + const state = get() + const flow = state.getFlowOrNull(flowId, identifiersOverride) + if (!flow) throw new Error('No flow found') + return flow.transactionIds.map((transactionId) => { + const transactionKey = getTransactionKey({ transactionId, ...flow }) + const transaction = state.transactions.get(transactionKey) + if (!transaction) throw new Error('No transaction found') + return transaction + }) + }, + isFlowResumable: (flowId, identifiersOverride) => { + const state = get() + const flow = state.getFlowOrNull(flowId, identifiersOverride) + if (!flow) return false + if (getCanRemoveFlow(state, flow)) return false + return true + }, + showFlowInput: (flowId, { input, disableBackgroundClick }, identifiersOverride) => + set((mutable) => { + const identifiers = getIdentifiers(mutable, identifiersOverride) + const flowKey = getFlowKey({ flowId, ...identifiers }) + mutable.flows.set(flowKey, { + ...identifiers, + flowId, + currentStage: 'input', + currentTransactionIndex: 0, + transactionIds: [], + input: input as WritableDraft, + disableBackgroundClick, + }) + mutable.current.flowId = flowId + }), + startFlow: ({ transactions, ...flow }, identifiersOverride) => { + const { flowId } = flow + const state = get() + const identifiers = getIdentifiers(state, identifiersOverride) + const flowKey = getFlowKey({ flowId, ...identifiers }) + const currentStage = (() => { + if (flow.intro) return 'intro' as const + if (flow.input) return 'input' as const + return 'transaction' as const + })() + set((mutable) => { + mutable.flows.set(flowKey, { + ...flow, + ...identifiers, + transactionIds: [], + flowId, + currentTransactionIndex: 0, + currentStage, + } as WritableDraft) + mutable.current.flowId = flowId + }) + state.setFlowTransactions(flowId, transactions, identifiers) + }, + resumeFlow: (flowId, identifiersOverride) => + set((mutable) => { + const identifiers = getIdentifiers(mutable, identifiersOverride) + const flowKey = getFlowKey({ flowId, ...identifiers }) + const flow = mutable.flows.get(flowKey) + // item no longer exists because transactions were completed + if (!flow) return + if (flow.intro) flow.currentStage = 'intro' + mutable.current.flowId = flowId + }), + resumeFlowWithCheck: (flowId, { push }, identifiersOverride) => { + const state = get() + const identifiers = getIdentifiers(state, identifiersOverride) + const flowKey = getFlowKey({ flowId, ...identifiers }) + const flow = state.flows.get(flowKey) + if (!flow) return + if (flow.resumeLink && getAllTransactionsComplete(state, flow)) { + push(flow.resumeLink) + return + } + state.resumeFlow(flowId, identifiers) + }, + cleanupFlowUnsafe: (flowId, identifiersOverride) => + set((mutable) => { + const identifiers = getIdentifiers(mutable, identifiersOverride) + const flowKey = getFlowKey({ flowId, ...identifiers }) + const flow = mutable.flows.get(flowKey) + + if (!flow) return + + for (const transaction of Object.values(mutable.transactions)) { + if ( + transaction?.flowId === flowId && + identifiers.account === transaction.account && + identifiers.sourceChainId === transaction.sourceChainId + ) { + mutable.transactions.delete(getTransactionKey(transaction)) + } + } + + mutable.flows.delete(flowKey) + }), + cleanupFlow: (flowId, identifiersOverride) => { + const state = get() + const flow = state.getFlowOrNull(flowId, identifiersOverride) + if (!flow) return + if (flow.requiresManualCleanup) return + if (flow.resumable) return + if (!getAllTransactionsComplete(state, flow)) return + state.cleanupFlowUnsafe(flowId, identifiersOverride) + }, + setFlowTransactions: (flowId, transactions, identifiersOverride) => + set((mutable) => { + const identifiers = getIdentifiers(mutable, identifiersOverride) + const flowKey = getFlowKey({ flowId, ...identifiers }) + const flow = mutable.flows.get(flowKey) + if (!flow) throw new Error('No flow found') + flow.transactionIds = [] + for (let i = 0; i < transactions.length; i += 1) { + const transaction = transactions[i] + const transactionId = `${transaction.name}-${i}` as const + flow.transactionIds.push(transactionId) + const transactionKey = getTransactionKey({ transactionId, ...flow }) + mutable.transactions.set(transactionKey, { + ...transaction, + targetChainId: transaction.targetChainId ?? flow.sourceChainId, + flowId: flow.flowId, + transactionId, + sourceChainId: flow.sourceChainId, + account: flow.account, + currentHash: null, + status: 'empty', + transactionType: null, + } as WritableDraft) + } + }), + getCurrentFlowTransactions: () => { + const state = get() + const flow = getCurrentFlowOrNull(state) + if (!flow) return [] + return state.getFlowTransactions(flow.flowId, flow) + }, + getCurrentOrPreviousFlow: () => { + const state = get() + const { account, sourceChainId, flowId: flowId_, _previousFlowId } = state.current + if (!account || !sourceChainId) return { flow: null, isPrevious: false } + + const isPrevious = !flowId_ && !!_previousFlowId + const flowId = isPrevious ? _previousFlowId : flowId_ ?? '' + const flowKey = getFlowKey({ account, sourceChainId, flowId }) + const flow = state.flows.get(flowKey) + if (!flow) return { flow: null, isPrevious: false } + return { flow, isPrevious } + }, + setCurrentFlowTransactions: (transactions) => { + const state = get() + const flow = getCurrentFlow(state) + state.setFlowTransactions(flow.flowId, transactions, flow) + }, + setCurrentFlowStage: (stage) => + set((mutable) => { + const flow = getCurrentFlow(mutable) + flow.currentStage = stage + }), + stopCurrentFlow: () => { + const flow = getCurrentFlow(get()) + set((mutable) => { + mutable.current._previousFlowId = flow.flowId + mutable.current.flowId = null + }) + setTimeout(() => { + get().cleanupFlow(flow.flowId, flow) + set((mutable) => { + mutable.current._previousFlowId = null + }) + }, 350) + }, + attemptCurrentFlowDismiss: () => { + const state = get() + const flow = getCurrentFlowOrNull(state) + if (!flow) return + if (flow.disableBackgroundClick && flow.currentStage === 'input') return + return state.stopCurrentFlow() + }, + incrementCurrentFlowTransactionIndex: () => + set((mutable) => { + const flow = getCurrentFlow(mutable) + flow.currentTransactionIndex += 1 + }), + resetCurrentFlowTransactionIndex: () => + set((mutable) => { + const flow = getCurrentFlow(mutable) + flow.currentTransactionIndex = 0 + }), +}) diff --git a/src/transaction/slices/createNotificationSlice.ts b/src/transaction/slices/createNotificationSlice.ts new file mode 100644 index 000000000..e9a5e4d9e --- /dev/null +++ b/src/transaction/slices/createNotificationSlice.ts @@ -0,0 +1,58 @@ +/* eslint-disable no-param-reassign */ +import type { WritableDraft } from 'immer/dist/internal' +import type { StateCreator } from 'zustand' + +import { onTransactionUpdateAnalytics } from '../analytics' +import type { StoredTransaction } from './createTransactionSlice' +import type { AllSlices, MiddlewareArray } from './types' +import { getIdentifiersOrNull } from './utils' + +type SuccessOrRevertedTransaction = Extract + +export type NotificationSlice = { + notificationBacklog: SuccessOrRevertedTransaction[] + currentNotification: SuccessOrRevertedTransaction | null + transactionDidUpdate: (transaction: StoredTransaction) => void + dismissNotification: () => void + clearNotifications: () => void +} + +export const createNotificationSlice: StateCreator< + AllSlices, + MiddlewareArray, + [], + NotificationSlice +> = (set) => ({ + notificationBacklog: [], + currentNotification: null, + transactionDidUpdate: (transaction) => + set((mutable) => { + const identifiers = getIdentifiersOrNull(mutable, undefined) + if (!identifiers) return + if (transaction.status !== 'success' && transaction.status !== 'reverted') return + if (transaction.status === 'success') onTransactionUpdateAnalytics(transaction) + if (mutable.currentNotification) { + mutable.notificationBacklog.push({ + ...transaction, + } as WritableDraft) + return + } + + mutable.currentNotification = transaction as WritableDraft + }), + dismissNotification: () => + set((mutable) => { + if (mutable.notificationBacklog.length > 0) { + mutable.currentNotification = + mutable.notificationBacklog.pop() as WritableDraft + return + } + + mutable.currentNotification = null + }), + clearNotifications: () => + set((mutable) => { + mutable.notificationBacklog = [] + mutable.currentNotification = null + }), +}) diff --git a/src/transaction/slices/createRegistrationFlowSlice.ts b/src/transaction/slices/createRegistrationFlowSlice.ts new file mode 100644 index 000000000..4e821a335 --- /dev/null +++ b/src/transaction/slices/createRegistrationFlowSlice.ts @@ -0,0 +1,516 @@ +import { zeroAddress, zeroHash, type Address, type Hex } from 'viem' +import type { StateCreator } from 'zustand' + +import { randomSecret } from '@ensdomains/ensjs/utils' + +import { childFuseObj } from '@app/components/@molecules/BurnFuses/BurnFusesContent' +import type { InitiateMoonpayRegistrationMutationResult } from '@app/components/pages/register/useMoonpayRegistration' +import { mainnetWithEns, type SourceChain } from '@app/constants/chains' +import type { ProfileRecord } from '@app/constants/profileRecordOptions' +import { getRegistrationParams } from '@app/hooks/useRegistrationParams' +import type { CurrentChildFuses } from '@app/types' +import { getSupportedChainContractAddress } from '@app/utils/getSupportedChainContractAddress' +import { wagmiConfig } from '@app/utils/query/wagmi' +import { secondsToYears, yearsToSeconds } from '@app/utils/time' + +import { getFlowKey } from '../key' +import type { TransactionStoreIdentifiers } from '../types' +import type { StoredTransaction } from './createTransactionSlice' +import type { AllSlices, MiddlewareArray } from './types' +import { getIdentifiers } from './utils' + +export type RegistrationFlowStep = 'pricing' | 'profile' | 'info' | 'transactions' | 'complete' +export type RegistrationPaymentMethod = 'ethereum' | 'moonpay' +export type RegistrationDurationType = 'date' | 'years' + +type RegistrationName = string +type RegistrationFlowKey = `["${RegistrationName}",${SourceChain['id']},"${Address}"]` + +type RegistrationPricingStepData = { + seconds: number + reverseRecord: boolean + paymentMethodChoice: RegistrationPaymentMethod + durationType: RegistrationDurationType +} + +type RegistrationProfileStepData = { + records: ProfileRecord[] + resolverAddress: Address | null + clearRecords?: boolean + permissions?: CurrentChildFuses +} + +type RegistrationTransactionsStepData = { + secret: Hex + isStarted: boolean +} + +type RegistrationFlowData = RegistrationPricingStepData & + RegistrationProfileStepData & + RegistrationTransactionsStepData + +type RegistrationFlowIdentifiers = TransactionStoreIdentifiers & { + name: RegistrationName +} + +type MoonpayExternalTransactionData = { + type: 'moonpay' + id: string + url: string +} + +export type StoredRegistrationFlow = RegistrationFlowIdentifiers & + Required & { + stepIndex: number + queue: RegistrationFlowStep[] + externalTransactionData: MoonpayExternalTransactionData | null + } + +export type RegistrationFlowSlice = { + registrationFlows: Map + + getRegistrationFlow: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => StoredRegistrationFlow | null + getCurrentRegistrationFlowOrDefault: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => StoredRegistrationFlow + getCurrentRegistrationFlowStep: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => RegistrationFlowStep + getCurrentCommitTransaction: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => StoredTransaction | null + getCurrentRegisterTransaction: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => StoredTransaction | null + + increaseRegistrationFlowStep: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + decreaseRegistrationFlowStep: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + setRegistrationFlowQueue: ( + name: RegistrationName, + queue: RegistrationFlowStep[], + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + setRegistrationPricingData: ( + name: RegistrationName, + pricingData: RegistrationPricingStepData, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + setRegistrationProfileData: ( + name: RegistrationName, + profileData: Partial, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + setRegistrationTransactionsData: ( + name: RegistrationName, + transactionsData: RegistrationTransactionsStepData, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + clearRegistrationFlow: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + setRegistrationFlowStarted: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + resetRegistrationFlow: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + resetRegistrationSecret: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + setRegistrationExternalTransactionData: ( + name: RegistrationName, + externalTransactionData: MoonpayExternalTransactionData, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + onRegistrationMoonpayTransactionCompleted: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + traverseRegistrationFlow: ( + name: RegistrationName, + data: { back: boolean }, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + createRegistrationFlow: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + + onRegistrationPricingStepCompleted: ( + name: RegistrationName, + data: RegistrationPricingStepData & { + resolverExists: boolean + initiateMoonpayRegistrationMutation: InitiateMoonpayRegistrationMutationResult + }, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + onRegistrationProfileStepCompleted: ( + name: RegistrationName, + data: RegistrationProfileStepData & { back: boolean }, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + onRegistrationInfoStepCompleted: ( + name: RegistrationName, + data: { back: boolean }, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + onRegistrationTransactionsStepCompleted: ( + name: RegistrationName, + data: { back: boolean; resetSecret?: boolean }, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + + startCommitNameTransaction: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + startRegisterNameTransaction: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + resumeCommitNameTransaction: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + resumeRegisterNameTransaction: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void + resetRegistrationTransactions: ( + name: RegistrationName, + identifiersOverride?: TransactionStoreIdentifiers, + ) => void +} + +const getIdentifiersWithDefault = ( + state: AllSlices, + identifiersOverride?: TransactionStoreIdentifiers, +) => { + const { account, sourceChainId } = identifiersOverride ?? state.current + return { account: account ?? zeroAddress, sourceChainId: sourceChainId ?? mainnetWithEns.id } +} + +const getCurrentRegistrationFlow = ( + name: RegistrationName, + state: AllSlices, + identifiersOverride?: TransactionStoreIdentifiers, +) => { + const { account, sourceChainId } = getIdentifiers(state, identifiersOverride) + + const registrationFlowKey = getFlowKey({ flowId: name, sourceChainId, account }) + const registrationFlow = state.registrationFlows.get(registrationFlowKey) + if (!registrationFlow) throw new Error('No registration flow found') + return registrationFlow +} + +const createDefaultRegistrationFlowData = ( + identifiers: RegistrationFlowIdentifiers, +): StoredRegistrationFlow => ({ + stepIndex: 0, + queue: ['pricing', 'info', 'transactions', 'complete'], + seconds: yearsToSeconds(1), + reverseRecord: false, + records: [], + resolverAddress: '0x', + permissions: childFuseObj, + secret: zeroHash, + isStarted: false, + paymentMethodChoice: 'ethereum', + externalTransactionData: null, + durationType: 'years', + clearRecords: false, + ...identifiers, +}) + +const getCommitTransactionFlowId = (name: RegistrationName) => `commit-${name}` +const getRegisterTransactionFlowId = (name: RegistrationName) => `register-${name}` + +export const createRegistrationFlowSlice: StateCreator< + AllSlices, + MiddlewareArray, + [], + RegistrationFlowSlice +> = (set, get) => ({ + registrationFlows: new Map(), + getRegistrationFlow: (name, identifiersOverride) => { + const state = get() + const identifiers = getIdentifiers(state, identifiersOverride) + const registrationFlowKey = getFlowKey({ flowId: name, ...identifiers }) + return state.registrationFlows.get(registrationFlowKey) ?? null + }, + getCurrentRegistrationFlowOrDefault: (name, identifiersOverride) => { + const state = get() + const identifiers = getIdentifiersWithDefault(state, identifiersOverride) + const registrationFlowKey = getFlowKey({ flowId: name, ...identifiers }) + const existing = state.registrationFlows.get(registrationFlowKey) + return existing ?? createDefaultRegistrationFlowData({ name, ...identifiers }) + }, + getCurrentRegistrationFlowStep: (name, identifiersOverride) => { + const state = get() + const currentRegistrationFlow = state.getCurrentRegistrationFlowOrDefault( + name, + identifiersOverride, + ) + return currentRegistrationFlow.queue[currentRegistrationFlow.stepIndex] + }, + getCurrentCommitTransaction: (name, identifiersOverride) => { + const state = get() + const identifiers = getIdentifiers(state, identifiersOverride) + const flowId = getCommitTransactionFlowId(name) + const transactions = state.getFlowTransactionsOrNull(flowId, identifiers) + return transactions?.[0] ?? null + }, + getCurrentRegisterTransaction: (name, identifiersOverride) => { + const state = get() + const identifiers = getIdentifiers(state, identifiersOverride) + const flowId = getRegisterTransactionFlowId(name) + const transactions = state.getFlowTransactionsOrNull(flowId, identifiers) + return transactions?.[0] ?? null + }, + increaseRegistrationFlowStep: (name, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + registrationFlow.stepIndex += 1 + }), + decreaseRegistrationFlowStep: (name, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + registrationFlow.stepIndex -= 1 + }), + setRegistrationFlowQueue: (name, queue, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + registrationFlow.queue = queue + }), + setRegistrationPricingData: (name, pricingData, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + registrationFlow.seconds = pricingData.seconds + registrationFlow.reverseRecord = pricingData.reverseRecord + registrationFlow.paymentMethodChoice = pricingData.paymentMethodChoice + registrationFlow.durationType = pricingData.durationType + }), + setRegistrationProfileData: (name, profileData, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + if (profileData.records) registrationFlow.records = profileData.records + if (profileData.permissions) registrationFlow.permissions = profileData.permissions + if (profileData.resolverAddress) + registrationFlow.resolverAddress = profileData.resolverAddress + }), + setRegistrationTransactionsData: (name, transactionsData, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + registrationFlow.secret = transactionsData.secret + registrationFlow.isStarted = transactionsData.isStarted + }), + clearRegistrationFlow: (name, identifiersOverride) => + set((state) => { + const identifiers = getIdentifiersWithDefault(state, identifiersOverride) + const registrationFlowKey = getFlowKey({ flowId: name, ...identifiers }) + state.registrationFlows.delete(registrationFlowKey) + state.cleanupFlowUnsafe(getCommitTransactionFlowId(name), identifiers) + state.cleanupFlowUnsafe(getRegisterTransactionFlowId(name), identifiers) + }), + setRegistrationFlowStarted: (name, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + registrationFlow.isStarted = true + }), + resetRegistrationFlow: (name, identifiersOverride) => + set((state) => { + const identifiers = getIdentifiersWithDefault(state, identifiersOverride) + const registrationFlowKey = getFlowKey({ flowId: name, ...identifiers }) + state.registrationFlows.set( + registrationFlowKey, + createDefaultRegistrationFlowData({ name, ...identifiers }), + ) + }), + resetRegistrationSecret: (name, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + registrationFlow.secret = randomSecret({ platformDomain: 'enslabs.eth', campaign: 3 }) + }), + setRegistrationExternalTransactionData: (name, externalTransactionData, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + registrationFlow.externalTransactionData = externalTransactionData + }), + onRegistrationMoonpayTransactionCompleted: (name, identifiersOverride) => + set((state) => { + const registrationFlow = getCurrentRegistrationFlow(name, state, identifiersOverride) + registrationFlow.externalTransactionData = null + registrationFlow.stepIndex = registrationFlow.queue.findIndex((step) => step === 'complete') + }), + traverseRegistrationFlow: (name, data, identifiersOverride) => { + const state = get() + if (data.back) state.decreaseRegistrationFlowStep(name, identifiersOverride) + else state.increaseRegistrationFlowStep(name, identifiersOverride) + }, + createRegistrationFlow: (name, identifiersOverride) => + set((state) => { + const identifiers = getIdentifiers(state, identifiersOverride) + const registrationFlowKey = getFlowKey({ flowId: name, ...identifiers }) + state.registrationFlows.set(registrationFlowKey, { + ...createDefaultRegistrationFlowData({ name, ...identifiers }), + secret: randomSecret({ platformDomain: 'enslabs.eth', campaign: 3 }), + }) + }), + onRegistrationPricingStepCompleted: (name, data, identifiersOverride) => { + const state = get() + const identifiers = getIdentifiers(state, identifiersOverride) + if (data.paymentMethodChoice === 'moonpay') { + data.initiateMoonpayRegistrationMutation.mutate({ + address: identifiers.account, + chainId: identifiers.sourceChainId, + duration: secondsToYears(data.seconds), + name, + }) + return + } + state.createRegistrationFlow(name, identifiers) + state.setRegistrationPricingData(name, data, identifiers) + let currentRegistrationFlow = state.getCurrentRegistrationFlowOrDefault(name, identifiers) + if (!currentRegistrationFlow.queue.includes('profile')) { + // if profile is not in queue, set the default profile data + const defaultResolverAddress = getSupportedChainContractAddress({ + client: wagmiConfig.getClient({ chainId: identifiers.sourceChainId }), + contract: 'ensPublicResolver', + }) + + state.setRegistrationProfileData(name, { + records: [{ key: 'eth', group: 'address', type: 'addr', value: identifiers.account }], + clearRecords: data.resolverExists, + resolverAddress: defaultResolverAddress, + }) + + if (data.reverseRecord) { + // if reverse record is selected, add the profile step to the queue + state.setRegistrationFlowQueue( + name, + ['pricing', 'profile', 'info', 'transactions', 'complete'], + identifiers, + ) + } + } + + currentRegistrationFlow = state.getCurrentRegistrationFlowOrDefault(name, identifiers) + // if profile is in queue and reverse record is selected, make sure that eth record is included and is set to address + if (currentRegistrationFlow.queue.includes('profile') && data.reverseRecord) { + const recordsWithoutEth = currentRegistrationFlow.records.filter( + (record) => record.key !== 'eth', + ) + const newRecords: ProfileRecord[] = [ + { key: 'eth', group: 'address', type: 'addr', value: identifiers.account }, + ...recordsWithoutEth, + ] + state.setRegistrationProfileData(name, { records: newRecords }, identifiers) + } + + state.increaseRegistrationFlowStep(name, identifiers) + }, + onRegistrationProfileStepCompleted: (name, data, identifiersOverride) => { + const state = get() + state.setRegistrationProfileData( + name, + { records: data.records, resolverAddress: data.resolverAddress }, + identifiersOverride, + ) + state.traverseRegistrationFlow(name, data, identifiersOverride) + }, + onRegistrationInfoStepCompleted: (name, data, identifiersOverride) => { + const state = get() + state.traverseRegistrationFlow(name, data, identifiersOverride) + }, + onRegistrationTransactionsStepCompleted: (name, data, identifiersOverride) => { + const state = get() + if (data.resetSecret) state.resetRegistrationSecret(name, identifiersOverride) + state.traverseRegistrationFlow(name, data, identifiersOverride) + }, + startCommitNameTransaction: (name, identifiersOverride) => { + const state = get() + state.setRegistrationFlowStarted(name, identifiersOverride) + + const identifiers = getIdentifiers(state, identifiersOverride) + const currentRegistrationFlow = state.getCurrentRegistrationFlowOrDefault( + name, + identifiersOverride, + ) + const registrationParams = getRegistrationParams({ + name, + owner: identifiers.account, + registrationData: currentRegistrationFlow, + }) + + const flowId = getCommitTransactionFlowId(name) + state.startFlow({ + flowId, + transactions: [ + { + name: 'commitName', + data: registrationParams, + }, + ], + requiresManualCleanup: true, + autoClose: true, + resumeLink: `/register/${name}`, + }) + }, + startRegisterNameTransaction: (name, identifiersOverride) => { + const state = get() + const identifiers = getIdentifiers(state, identifiersOverride) + const currentRegistrationFlow = state.getCurrentRegistrationFlowOrDefault(name, identifiers) + const registrationParams = getRegistrationParams({ + name, + owner: identifiers.account, + registrationData: currentRegistrationFlow, + }) + + const flowId = getRegisterTransactionFlowId(name) + state.startFlow({ + flowId, + transactions: [ + { + name: 'registerName', + data: registrationParams, + }, + ], + requiresManualCleanup: true, + autoClose: true, + resumeLink: `/register/${name}`, + }) + }, + resumeCommitNameTransaction: (name, identifiersOverride) => { + const state = get() + state.resumeFlow(getCommitTransactionFlowId(name), identifiersOverride) + }, + resumeRegisterNameTransaction: (name, identifiersOverride) => { + const state = get() + state.resumeFlow(getRegisterTransactionFlowId(name), identifiersOverride) + }, + resetRegistrationTransactions: (name, identifiersOverride) => { + const state = get() + const identifiers = getIdentifiers(state, identifiersOverride) + state.cleanupFlowUnsafe(getCommitTransactionFlowId(name), identifiers) + state.cleanupFlowUnsafe(getRegisterTransactionFlowId(name), identifiers) + state.resetRegistrationSecret(name, identifiers) + state.traverseRegistrationFlow(name, { back: true }, identifiers) + }, +}) diff --git a/src/transaction/slices/createTransactionSlice.ts b/src/transaction/slices/createTransactionSlice.ts new file mode 100644 index 000000000..6f9257be7 --- /dev/null +++ b/src/transaction/slices/createTransactionSlice.ts @@ -0,0 +1,239 @@ +import type { Address, Hash, Hex } from 'viem' +import type { StateCreator } from 'zustand' + +import type { SourceChain, TargetChain } from '@app/constants/chains' + +import { getTransactionKey } from '../key' +import type { TransactionStoreIdentifiers } from '../types' +import type { UserTransactionData, UserTransactionName } from '../user/transaction' +import { + getAllTransactionsComplete, + getCanRemoveFlow, + getCurrentFlowOrNull, + type FlowId, +} from './createFlowSlice' +import type { AllSlices, MiddlewareArray } from './types' +import { compareFlow, getIdentifiersOrNull, getStoredTransaction } from './utils' + +type TransactionIndex = number +export type TransactionId = `${UserTransactionName}-${TransactionIndex}` +export type TransactionKey = `["${TransactionId}","${FlowId}",${SourceChain['id']},"${Address}"]` +export type StoredTransactionStatus = + | 'empty' + | 'waitingForUser' + | 'pending' + | 'success' + | 'reverted' +export type StoredTransactionType = 'standard' | 'safe' + +type TransactionSubmission = { + input: Hex + timestamp: number + nonce: number +} + +type EmptyStoredTransaction = { + status: 'empty' + currentHash: null + transactionType: null + transaction?: never + receipt?: never + search?: never +} + +type WaitingForUserStoredTransaction = { + status: 'waitingForUser' + currentHash: null + transactionType: StoredTransactionType + transaction: TransactionSubmission + receipt?: never +} + +type PendingStoredTransaction = { + status: 'pending' + currentHash: Hash + transactionType: StoredTransactionType +} + +type SuccessStoredTransaction = { + status: 'success' + currentHash: Hash + transactionType: StoredTransactionType + receipt: ReceiptData +} + +type RevertedStoredTransaction = { + status: 'reverted' + currentHash: Hash + transactionType: StoredTransactionType + receipt: ReceiptData +} + +export type StoredTransactionIdentifiers = TransactionStoreIdentifiers & { + targetChainId: TargetChain['id'] + flowId: FlowId + transactionId: TransactionId +} + +type ReceiptData = { + // TODO(tate): idk what we need from this yet + timestamp: number +} + +export type GenericStoredTransaction< + name extends UserTransactionName = UserTransactionName, + status extends StoredTransactionStatus = StoredTransactionStatus, +> = StoredTransactionIdentifiers & { + name: name + data: UserTransactionData + status: status + currentHash: Hash | null + transactionType: StoredTransactionType | null + + submission?: + | TransactionSubmission + | { + timestamp: number + } + receipt?: ReceiptData + search?: { + retries: number + status: 'searching' | 'found' + } +} & ( + | EmptyStoredTransaction + | WaitingForUserStoredTransaction + | PendingStoredTransaction + | SuccessStoredTransaction + | RevertedStoredTransaction + ) + +export type StoredTransaction< + status extends StoredTransactionStatus = StoredTransactionStatus, + other = {}, +> = { + [action in UserTransactionName]: GenericStoredTransaction & other +}[UserTransactionName] + +export type StoredTransactionList< + status extends StoredTransactionStatus = StoredTransactionStatus, +> = StoredTransaction[] + +export type StoredTransactionResult< + status extends StoredTransactionStatus | StoredTransactionStatus[], +> = StoredTransaction + +export type TransactionSlice = { + transactions: Map + getTransaction: (identifiers: StoredTransactionIdentifiers) => StoredTransaction | null + getTransactionsByStatus: ( + status: status, + ) => StoredTransactionResult[] + getAllTransactions: () => StoredTransaction[] + isTransactionResumable: (transaction: StoredTransaction) => boolean + setTransactionStatus: ( + identifiers: StoredTransactionIdentifiers, + status: StoredTransactionStatus, + ) => void + setTransactionReceipt: (identifiers: StoredTransactionIdentifiers, receipt: ReceiptData) => void + setTransactionHash: (identifiers: StoredTransactionIdentifiers, hash: Hash) => void + setTransactionSubmission: ( + identifiers: StoredTransactionIdentifiers, + submission: TransactionSubmission & Pick, + ) => void + clearTransactionsAndFlows: () => void +} + +export const createTransactionSlice: StateCreator< + AllSlices, + MiddlewareArray, + [], + TransactionSlice +> = (set, get) => ({ + transactions: new Map(), + getTransaction: (identifiers) => { + const state = get() + return state.transactions.get(getTransactionKey(identifiers)) ?? null + }, + getTransactionsByStatus: ( + status: status, + ) => { + const state = get() + const identifiers = getIdentifiersOrNull(state, undefined) + const statusArray: StoredTransactionStatus[] = Array.isArray(status) ? status : [status] + if (!identifiers) return [] + return Array.from(state.transactions.values()).filter( + (t): t is StoredTransactionResult => + !!t && + statusArray.includes(t.status) && + t.sourceChainId === identifiers.sourceChainId && + t.account === identifiers.account, + ) + }, + getAllTransactions: () => { + const state = get() + const identifiers = getIdentifiersOrNull(state, undefined) + if (!identifiers) return [] + return Array.from(state.transactions.values()).filter( + (t): t is StoredTransaction => + !!t && t.sourceChainId === identifiers.sourceChainId && t.account === identifiers.account, + ) + }, + isTransactionResumable: (transaction) => { + const state = get() + const flow = state.getFlowOrNull(transaction.flowId, transaction) + if (!flow) return false + if (getCanRemoveFlow(state, flow)) return false + const transactionIndex = flow.transactionIds.indexOf(transaction.transactionId) + return transactionIndex === flow.currentTransactionIndex + }, + setTransactionStatus: (identifiers, status) => { + set((mutable) => { + const transaction = getStoredTransaction(mutable, identifiers) + transaction.status = status + }) + const state = get() + const transaction = getStoredTransaction(state, identifiers) + state.transactionDidUpdate(transaction) + + const flow = state.getFlowOrNull(transaction.flowId, transaction) + if (!flow) return + if (!flow.autoClose) return + if (!getAllTransactionsComplete(state, flow)) return + + const currentFlow = getCurrentFlowOrNull(state) + if (!currentFlow) return + const isEqual = compareFlow(currentFlow, flow) + if (!isEqual) return + state.stopCurrentFlow() + }, + setTransactionReceipt: (identifiers, receipt) => + set((mutable) => { + const transaction = getStoredTransaction(mutable, identifiers) + transaction.receipt = receipt + }), + setTransactionHash: (identifiers, hash) => { + set((mutable) => { + const transaction = getStoredTransaction(mutable, identifiers) + transaction.currentHash = hash + }) + const state = get() + const transaction = getStoredTransaction(state, identifiers) + if (transaction.status === 'empty' || transaction.status === 'waitingForUser') + state.setTransactionStatus(identifiers, 'pending') + }, + setTransactionSubmission: (identifiers, { transactionType, ...submission }) => { + set((mutable) => { + const transaction = getStoredTransaction(mutable, identifiers) + transaction.submission = submission + transaction.transactionType = transactionType + }) + const state = get() + state.setTransactionStatus(identifiers, 'waitingForUser') + }, + clearTransactionsAndFlows: () => + set((mutable) => { + mutable.transactions.clear() + mutable.flows.clear() + }), +}) diff --git a/src/transaction/slices/types.ts b/src/transaction/slices/types.ts new file mode 100644 index 000000000..4c1ce322b --- /dev/null +++ b/src/transaction/slices/types.ts @@ -0,0 +1,17 @@ +import type { CurrentSlice } from './createCurrentSlice' +import type { FlowSlice } from './createFlowSlice' +import type { NotificationSlice } from './createNotificationSlice' +import type { RegistrationFlowSlice } from './createRegistrationFlowSlice' +import type { TransactionSlice } from './createTransactionSlice' + +export type AllSlices = FlowSlice & + CurrentSlice & + TransactionSlice & + NotificationSlice & + RegistrationFlowSlice + +export type MiddlewareArray = [ + ['zustand/persist', unknown], + ['zustand/subscribeWithSelector', never], + ['zustand/immer', never], +] diff --git a/src/transaction/slices/utils.ts b/src/transaction/slices/utils.ts new file mode 100644 index 000000000..88f29ced8 --- /dev/null +++ b/src/transaction/slices/utils.ts @@ -0,0 +1,43 @@ +import { getTransactionKey } from '../key' +import type { TransactionStoreIdentifiers } from '../types' +import type { CurrentSlice } from './createCurrentSlice' +import type { StoredFlow } from './createFlowSlice' +import type { StoredTransactionIdentifiers } from './createTransactionSlice' +import type { AllSlices } from './types' + +export const getIdentifiersOrNull = ( + state: CurrentSlice, + identifiersOverride: TransactionStoreIdentifiers | undefined, +) => { + const { account, sourceChainId } = identifiersOverride ?? state.current + if (!account) return null + if (!sourceChainId) return null + return { account, sourceChainId } +} + +export const getIdentifiers = ( + state: CurrentSlice, + identifiersOverride: TransactionStoreIdentifiers | undefined, +) => { + const { account, sourceChainId } = identifiersOverride ?? state.current + if (!account) throw new Error('No account found') + if (!sourceChainId) throw new Error('No sourceChainId found') + return { account, sourceChainId } +} + +export const getStoredTransaction = ( + state: AllSlices, + identifiers: StoredTransactionIdentifiers, +) => { + const transactionKey = getTransactionKey(identifiers) + const transaction = state.transactions.get(transactionKey) + if (!transaction) throw new Error('No transaction found') + return transaction +} + +export const compareFlow = (a: StoredFlow, b: StoredFlow) => { + if (a.flowId !== b.flowId) return false + if (a.account !== b.account) return false + if (a.sourceChainId !== b.sourceChainId) return false + return true +} diff --git a/src/transaction/transactionManager.ts b/src/transaction/transactionManager.ts new file mode 100644 index 000000000..17c0af60e --- /dev/null +++ b/src/transaction/transactionManager.ts @@ -0,0 +1,58 @@ +import { del as idbDel, get as idbGet, set as idbSet } from 'idb-keyval' +import { enableMapSet } from 'immer' +import { persist, subscribeWithSelector } from 'zustand/middleware' +import { immer } from 'zustand/middleware/immer' +import { createWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/vanilla/shallow' + +import { existingCommitListener } from './listeners/existingCommitListener' +import { transactionReceiptListener } from './listeners/transactionReceiptListener' +import { createCurrentSlice } from './slices/createCurrentSlice' +import { createFlowSlice } from './slices/createFlowSlice' +import { createNotificationSlice } from './slices/createNotificationSlice' +import { createRegistrationFlowSlice } from './slices/createRegistrationFlowSlice' +import { createTransactionSlice } from './slices/createTransactionSlice' +import type { AllSlices } from './slices/types' + +enableMapSet() + +export const useTransactionManager = createWithEqualityFn()( + persist( + subscribeWithSelector( + immer((...a) => ({ + ...createCurrentSlice(...a), + ...createFlowSlice(...a), + ...createTransactionSlice(...a), + ...createNotificationSlice(...a), + ...createRegistrationFlowSlice(...a), + })), + ), + { + name: 'transaction-data', + storage: { + getItem: async (name) => { + const value = await idbGet(name) + return value ?? null + }, + setItem: idbSet, + removeItem: idbDel, + }, + onRehydrateStorage: (state) => { + return () => state._setHasHydrated(true) + }, + skipHydration: typeof window === 'undefined', + partialize: (state) => + ({ + flows: state.flows, + transactions: state.transactions, + registrationFlows: state.registrationFlows, + }) as Pick, + }, + ), + shallow, +) + +export type UseTransactionManager = typeof useTransactionManager + +useTransactionManager.subscribe(...transactionReceiptListener(useTransactionManager)) +useTransactionManager.subscribe(...existingCommitListener(useTransactionManager)) diff --git a/src/transaction/types.ts b/src/transaction/types.ts new file mode 100644 index 000000000..58cebedb4 --- /dev/null +++ b/src/transaction/types.ts @@ -0,0 +1,292 @@ +// import type { TOptions } from 'i18next' +// import type { WritableDraft } from 'immer/dist/internal' +// import type { ComponentProps } from 'react' +// import type { Address, Hash, Hex } from 'viem' + +import type { Address } from 'viem' + +import type { SourceChain } from '@app/constants/chains' + +// import type { SourceChain, TargetChain } from '@app/constants/chains' +// import type { IntroComponentName } from '@app/transaction-flow/intro' + +// import type { DataInputComponent, DataInputName } from './user/input' +// import type { IntroComponent } from './user/intro' +// import type { TransactionData, TransactionItemUnion, TransactionName } from './user/transaction' + +// export type TransactionFlowStage = 'input' | 'intro' | 'transaction' +// export type StoredTransactionStatus = +// | 'empty' +// | 'waitingForUser' +// | 'pending' +// | 'success' +// | 'reverted' +// export type StoredTransactionType = 'standard' | 'safe' + +export type TransactionStoreIdentifiers = { + sourceChainId: SourceChain['id'] + account: Address +} +// export type FlowId = string +// export type FlowKey = `["${FlowId}",${SourceChain['id']},"${Address}"]` +// type TransactionIndex = number +// export type TransactionId = `${TransactionName}-${TransactionIndex}` +// export type TransactionKey = `["${TransactionId}","${FlowId}",${SourceChain['id']},"${Address}"]` + +// export type GenericDataInput< +// name extends DataInputName = DataInputName, +// data extends ComponentProps = ComponentProps, +// > = { +// name: name +// data: data +// } +// export type DataInput = { +// [name in DataInputName]: GenericDataInput +// }[DataInputName] + +// type GenericDataIntro< +// name extends IntroComponentName = IntroComponentName, +// data extends ComponentProps = ComponentProps, +// > = { +// name: name +// data: data +// } + +// export type DataIntro = { +// [name in IntroComponentName]: GenericDataIntro +// }[IntroComponentName] + +// type StoredTranslationReference = [key: string, options?: TOptions] + +// export type TransactionIntro = { +// title: StoredTranslationReference +// leadingLabel?: StoredTranslationReference +// trailingLabel?: StoredTranslationReference +// content: DataIntro +// } + +// type EmptyStoredTransaction = { +// status: 'empty' +// currentHash: null +// transactionType: null +// transaction?: never +// receipt?: never +// search?: never +// } + +// type WaitingForUserStoredTransaction = { +// status: 'waitingForUser' +// currentHash: null +// transactionType: StoredTransactionType +// transaction: { +// input: Hex +// timestamp: number +// nonce: number +// } +// receipt?: never +// } + +// type PendingStoredTransaction = { +// status: 'pending' +// currentHash: Hash +// transactionType: StoredTransactionType +// } + +// type SuccessStoredTransaction = { +// status: 'success' +// currentHash: Hash +// transactionType: StoredTransactionType +// } + +// type RevertedStoredTransaction = { +// status: 'reverted' +// currentHash: Hash +// transactionType: StoredTransactionType +// } + +// export type StoredTransactionIdentifiers = TransactionStoreIdentifiers & { +// targetChainId: TargetChain['id'] +// flowId: FlowId +// transactionId: TransactionId +// } + +// type TransactionSubmission = { +// input: Hex +// timestamp: number +// nonce: number +// } + +// export type GenericStoredTransaction< +// name extends TransactionName = TransactionName, +// status extends StoredTransactionStatus = StoredTransactionStatus, +// > = StoredTransactionIdentifiers & { +// name: name +// data: TransactionData +// status: status +// currentHash: Hash | null +// transactionType: StoredTransactionType | null + +// submission?: +// | { +// input: Hex +// timestamp: number +// nonce: number +// } +// | { +// timestamp: number +// } +// receipt?: { +// // TODO(tate): idk what we need from this yet +// } +// search?: { +// retries: number +// status: 'searching' | 'found' +// } +// } & ( +// | EmptyStoredTransaction +// | WaitingForUserStoredTransaction +// | PendingStoredTransaction +// | SuccessStoredTransaction +// | RevertedStoredTransaction +// ) + +// export type StoredTransaction< +// status extends StoredTransactionStatus = StoredTransactionStatus, +// other = {}, +// > = { +// [action in TransactionName]: GenericStoredTransaction & other +// }[TransactionName] + +// export type StoredFlow = TransactionStoreIdentifiers & { +// flowId: FlowId +// transactionIds: TransactionId[] +// currentTransactionIndex: number +// currentStage: TransactionFlowStage +// input?: DataInput +// intro?: TransactionIntro +// resumable?: boolean +// requiresManualCleanup?: boolean +// autoClose?: boolean +// resumeLink?: string +// disableBackgroundClick?: boolean +// } + +// export type FlowInitialiserData = Omit< +// StoredFlow, +// 'currentStage' | 'currentTransactionIndex' | 'transactionIds' | keyof TransactionStoreIdentifiers +// > & { +// transactions: TransactionItemUnion[] +// } + +// export type LastTransactionChange = StoredTransaction + +// export type TransactionStoreData = { +// flows: { +// [flowKey: FlowKey]: StoredFlow | undefined +// } +// transactions: { +// [transactionKey: TransactionKey]: StoredTransaction | undefined +// } +// current: { +// flowId: string | null +// sourceChainId: SourceChain['id'] | null +// account: Address | null +// _previousFlowId: string | null +// } +// lastTransactionChange: LastTransactionChange | null +// _hasHydrated: boolean +// } + +// export type WritableTransactionStoreData = WritableDraft + +// export type TransactionList = +// StoredTransaction[] + +// export type TransactionStoreFunctions = { +// flow: { +// helpers: { +// getAllTransactionsComplete: (flow: StoredFlow) => boolean +// getCanRemoveFlow: (flow: StoredFlow) => boolean +// getNoTransactionsStarted: (flow: StoredFlow) => boolean +// } +// showInput: ( +// flowId: FlowId, +// { +// input, +// disableBackgroundClick, +// }: { input: GenericDataInput; disableBackgroundClick?: boolean }, +// identifiersOverride?: TransactionStoreIdentifiers, +// ) => void +// start: (flow: FlowInitialiserData, identifiersOverride?: TransactionStoreIdentifiers) => void +// resume: (flowId: FlowId, identifiersOverride?: TransactionStoreIdentifiers) => void +// resumeWithCheck: ( +// flowId: FlowId, +// { push }: { push: (path: string) => void }, +// identifiersOverride?: TransactionStoreIdentifiers, +// ) => void +// getResumable: (flowId: FlowId, identifiersOverride?: TransactionStoreIdentifiers) => boolean +// cleanupUnsafe: (flowId: FlowId, identifiersOverride?: TransactionStoreIdentifiers) => void +// cleanup: (flowId: FlowId, identifiersOverride?: TransactionStoreIdentifiers) => void +// setTransactions: ( +// flowId: FlowId, +// transactions: TransactionItemUnion[], +// identifiersOverride?: TransactionStoreIdentifiers, +// ) => void +// getTransactions: ( +// flowId: FlowId, +// identifiersOverride?: TransactionStoreIdentifiers, +// ) => TransactionList +// getStageOrNull: ( +// flowId: FlowId, +// identifiersOverride?: TransactionStoreIdentifiers, +// ) => TransactionFlowStage | 'complete' | null +// getFlowOrNull: ( +// flowId: FlowId, +// identifiersOverride?: TransactionStoreIdentifiers, +// ) => StoredFlow | null +// current: { +// setTransactions: (transactions: TransactionItemUnion[]) => void +// setStage: ({ stage }: { stage: TransactionFlowStage }) => void +// stop: () => void +// selectedOrPrevious: () => { flow: StoredFlow | null; isPrevious: boolean } +// attemptDismiss: () => void +// incrementTransaction: () => void +// resetTransactionIndex: () => void +// getTransactions: () => TransactionList +// } +// } +// transaction: { +// setStatus: (identifiers: StoredTransactionIdentifiers, status: StoredTransactionStatus) => void +// setHash: (identifiers: StoredTransactionIdentifiers, hash: Hash) => void +// setSubmission: ( +// identifiers: StoredTransactionIdentifiers, +// submission: TransactionSubmission & Pick, +// ) => void +// getByStatus: (status: status) => TransactionList +// getAll: () => TransactionList +// getResumable: (transaction: StoredTransaction) => boolean +// } +// clear: () => void +// _setHasHydrated: (hasHydrated: boolean) => void +// } + +// export type NotificationQueueFunctions = { +// notificationQueue: { +// add: (transaction: StoredTransaction) => void +// consume: () => StoredTransaction | null +// } +// } + +// type NotificationQueueData = { +// notificationQueue: TransactionId[] +// } + +// type PlainTransactionStore = { +// _internal: TransactionStoreData +// } & TransactionStoreFunctions + +// type PlainNotificationQueue = { +// _internal: NotificationQueueData +// } & NotificationQueueFunctions + +// export type TransactionStore = PlainTransactionStore & PlainNotificationQueue diff --git a/src/transaction/usePreparedDataInput.ts b/src/transaction/usePreparedDataInput.ts new file mode 100644 index 000000000..588001695 --- /dev/null +++ b/src/transaction/usePreparedDataInput.ts @@ -0,0 +1,34 @@ +import type { ComponentProps } from 'react' +import { useAccount } from 'wagmi' + +import { useTransactionManager } from './transactionManager' +import { + transactionInputComponents, + type TransactionInputComponent, + type TransactionInputName, +} from './user/input' + +type ShowDataInput = ( + flowId: string, + data: ComponentProps['data'], + options?: { + disableBackgroundClick?: boolean + }, +) => void + +export const usePreparedDataInput = (name: name) => { + const showInput = useTransactionManager((s) => s.showFlowInput) + const { address } = useAccount() + if (address) (transactionInputComponents[name] as any).render.preload() + + const func: ShowDataInput = (flowId, data, options) => + showInput(flowId, { + input: { + name, + data: data as never, + }, + disableBackgroundClick: options?.disableBackgroundClick, + }) + + return func +} diff --git a/src/transaction-flow/input/index.tsx b/src/transaction/user/input.tsx similarity index 53% rename from src/transaction-flow/input/index.tsx rename to src/transaction/user/input.tsx index 4981b2402..7ad2bd4a0 100644 --- a/src/transaction-flow/input/index.tsx +++ b/src/transaction/user/input.tsx @@ -1,24 +1,23 @@ import dynamic from 'next/dynamic' -import { useContext, useEffect } from 'react' +import { useContext, useEffect, type ComponentProps } from 'react' -import DynamicLoadingContext from '@app/components/@molecules/TransactionDialogManager/DynamicLoadingContext' - -import TransactionLoader from '../TransactionLoader' -import type { Props as AdvancedEditorProps } from './AdvancedEditor/AdvancedEditor-flow' -import type { Props as CreateSubnameProps } from './CreateSubname-flow' -import type { Props as DeleteEmancipatedSubnameWarningProps } from './DeleteEmancipatedSubnameWarning/DeleteEmancipatedSubnameWarning-flow' -import type { Props as DeleteSubnameNotParentWarningProps } from './DeleteSubnameNotParentWarning/DeleteSubnameNotParentWarning-flow' -import type { Props as EditResolverProps } from './EditResolver/EditResolver-flow' -import type { Props as EditRolesProps } from './EditRoles/EditRoles-flow' -import type { Props as ExtendNamesProps } from './ExtendNames/ExtendNames-flow' -import type { Props as ProfileEditorProps } from './ProfileEditor/ProfileEditor-flow' -import type { Props as ResetPrimaryNameProps } from './ResetPrimaryName/ResetPrimaryName-flow' -import type { Props as RevokePermissionsProps } from './RevokePermissions/RevokePermissions-flow' -import type { Props as SelectPrimaryNameProps } from './SelectPrimaryName/SelectPrimaryName-flow' -import type { Props as SendNameProps } from './SendName/SendName-flow' -import type { Props as SyncManagerProps } from './SyncManager/SyncManager-flow' -import type { Props as UnknownLabelsProps } from './UnknownLabels/UnknownLabels-flow' -import type { Props as VerifyProfileProps } from './VerifyProfile/VerifyProfile-flow' +import DynamicLoadingContext from '../components/DynamicLoadingContext' +import TransactionLoader from '../components/TransactionLoader' +import type { Props as AdvancedEditorProps } from './input/AdvancedEditor/AdvancedEditor-flow' +import type { Props as CreateSubnameProps } from './input/CreateSubname/CreateSubname-flow' +import type { Props as DeleteEmancipatedSubnameWarningProps } from './input/DeleteEmancipatedSubnameWarning/DeleteEmancipatedSubnameWarning-flow' +import type { Props as DeleteSubnameNotParentWarningProps } from './input/DeleteSubnameNotParentWarning/DeleteSubnameNotParentWarning-flow' +import type { Props as EditResolverProps } from './input/EditResolver/EditResolver-flow' +import type { Props as EditRolesProps } from './input/EditRoles/EditRoles-flow' +import type { Props as ExtendNamesProps } from './input/ExtendNames/ExtendNames-flow' +import type { Props as ProfileEditorProps } from './input/ProfileEditor/ProfileEditor-flow' +import type { Props as ResetPrimaryNameProps } from './input/ResetPrimaryName/ResetPrimaryName-flow' +import type { Props as RevokePermissionsProps } from './input/RevokePermissions/RevokePermissions-flow' +import type { Props as SelectPrimaryNameProps } from './input/SelectPrimaryName/SelectPrimaryName-flow' +import type { Props as SendNameProps } from './input/SendName/SendName-flow' +import type { Props as SyncManagerProps } from './input/SyncManager/SyncManager-flow' +import type { Props as UnknownLabelsProps } from './input/UnknownLabels/UnknownLabels-flow' +import type { Props as VerifyProfileProps } from './input/VerifyProfile/VerifyProfile-flow' // Lazily load input components as needed const dynamicHelper = (name: string) => @@ -27,7 +26,7 @@ const dynamicHelper = (name: string) => import( /* webpackMode: "lazy" */ /* webpackExclude: /\.test.tsx$/ */ - `./${name}-flow` + `./input/${name}-flow` ), { loading: () => { @@ -44,7 +43,7 @@ const dynamicHelper = (name: string) => ) const AdvancedEditor = dynamicHelper('AdvancedEditor/AdvancedEditor') -const CreateSubname = dynamicHelper('CreateSubname') +const CreateSubname = dynamicHelper('CreateSubname/CreateSubname') const DeleteEmancipatedSubnameWarning = dynamicHelper( 'DeleteEmancipatedSubnameWarning/DeleteEmancipatedSubnameWarning', ) @@ -67,7 +66,7 @@ const SyncManager = dynamicHelper('SyncManager/SyncManager') const UnknownLabels = dynamicHelper('UnknownLabels/UnknownLabels') const VerifyProfile = dynamicHelper('VerifyProfile/VerifyProfile') -export const DataInputComponents = { +export const transactionInputComponents = { AdvancedEditor, CreateSubname, DeleteEmancipatedSubnameWarning, @@ -85,6 +84,19 @@ export const DataInputComponents = { VerifyProfile, } -export type DataInputName = keyof typeof DataInputComponents +export type TransactionInputName = keyof typeof transactionInputComponents + +export type TransactionInputComponent = typeof transactionInputComponents -export type DataInputComponent = typeof DataInputComponents +export type GenericTransactionInput< + name extends TransactionInputName = TransactionInputName, + data extends ComponentProps = ComponentProps< + TransactionInputComponent[name] + >, +> = { + name: name + data: data +} +export type TransactionInput = { + [name in TransactionInputName]: GenericTransactionInput +}[TransactionInputName] diff --git a/src/transaction-flow/input/AdvancedEditor/AdvancedEditor-flow.tsx b/src/transaction/user/input/AdvancedEditor/AdvancedEditor-flow.tsx similarity index 84% rename from src/transaction-flow/input/AdvancedEditor/AdvancedEditor-flow.tsx rename to src/transaction/user/input/AdvancedEditor/AdvancedEditor-flow.tsx index b7cbc4d59..a6f2cb76a 100644 --- a/src/transaction-flow/input/AdvancedEditor/AdvancedEditor-flow.tsx +++ b/src/transaction/user/input/AdvancedEditor/AdvancedEditor-flow.tsx @@ -10,10 +10,11 @@ import AdvancedEditorTabContent from '@app/components/@molecules/AdvancedEditor/ import AdvancedEditorTabs from '@app/components/@molecules/AdvancedEditor/AdvancedEditorTabs' import useAdvancedEditor from '@app/hooks/useAdvancedEditor' import { useProfile } from '@app/hooks/useProfile' -import { createTransactionItem, TransactionItem } from '@app/transaction-flow/transaction' -import type { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { StoredTransaction } from '@app/transaction/slices/createTransactionSlice' import { Profile } from '@app/types' +import type { TransactionDialogPassthrough } from '../../../components/TransactionDialogManager' + const NameContainer = styled.div(({ theme }) => [ css` display: block; @@ -61,12 +62,19 @@ export type Props = { onDismiss?: () => void } & TransactionDialogPassthrough -const AdvancedEditor = ({ data, transactions = [], dispatch, onDismiss }: Props) => { +const AdvancedEditor = ({ + data, + transactions = [], + onDismiss, + setTransactions, + setStage, +}: Props) => { const { t } = useTranslation('profile') const name = data?.name || '' const transaction = transactions.find( - (item: TransactionItem) => item.name === 'updateProfile', - ) as TransactionItem<'updateProfile'> + (item: StoredTransaction): item is Extract => + item.name === 'updateProfile', + ) const { data: fetchedProfile, isLoading: isProfileLoading } = useProfile({ name }) const [profile, setProfile] = useState(undefined) @@ -80,19 +88,19 @@ const AdvancedEditor = ({ data, transactions = [], dispatch, onDismiss }: Props) const handleCreateTransaction = useCallback( (records: RecordOptions) => { - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('updateProfile', { + setTransactions([ + { + name: 'updateProfile', + data: { name, resolverAddress: fetchedProfile!.resolverAddress!, records, - }), - ], - }) - dispatch({ name: 'setFlowStage', payload: 'transaction' }) + }, + }, + ]) + setStage('transaction') }, - [fetchedProfile, dispatch, name], + [fetchedProfile, setTransactions, setStage, name], ) const advancedEditorForm = useAdvancedEditor({ diff --git a/src/transaction-flow/input/AdvancedEditor/AdvancedEditor.test.tsx b/src/transaction/user/input/AdvancedEditor/AdvancedEditor.test.tsx similarity index 100% rename from src/transaction-flow/input/AdvancedEditor/AdvancedEditor.test.tsx rename to src/transaction/user/input/AdvancedEditor/AdvancedEditor.test.tsx diff --git a/src/transaction-flow/input/CreateSubname-flow.tsx b/src/transaction/user/input/CreateSubname/CreateSubname-flow.tsx similarity index 83% rename from src/transaction-flow/input/CreateSubname-flow.tsx rename to src/transaction/user/input/CreateSubname/CreateSubname-flow.tsx index a025c8aed..780bea278 100644 --- a/src/transaction-flow/input/CreateSubname-flow.tsx +++ b/src/transaction/user/input/CreateSubname/CreateSubname-flow.tsx @@ -6,10 +6,8 @@ import { validateName } from '@ensdomains/ensjs/utils' import { Button, Dialog, Input } from '@ensdomains/thorin' import useDebouncedCallback from '@app/hooks/useDebouncedCallback' - -import { useValidateSubnameLabel } from '../../hooks/useValidateSubnameLabel' -import { createTransactionItem } from '../transaction' -import { TransactionDialogPassthrough } from '../types' +import { useValidateSubnameLabel } from '@app/hooks/useValidateSubnameLabel' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' type Data = { parent: string @@ -29,7 +27,12 @@ const ParentLabel = styled.div( `, ) -const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Props) => { +const CreateSubname = ({ + data: { parent, isWrapped }, + onDismiss, + setStage, + setTransactions, +}: Props) => { const { t } = useTranslation('profile') const [label, setLabel] = useState('') @@ -48,20 +51,17 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro const isLoading = isUseValidateSubnameLabelLoading || !isLabelsInsync const handleSubmit = () => { - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('createSubname', { + setTransactions([ + { + name: 'createSubname', + data: { contract: isWrapped ? 'nameWrapper' : 'registry', label, parent, - }), - ], - }) - dispatch({ - name: 'setFlowStage', - payload: 'transaction', - }) + }, + }, + ]) + setStage('transaction') } return ( diff --git a/src/transaction-flow/input/DeleteEmancipatedSubnameWarning/DeleteEmancipatedSubnameWarning-flow.tsx b/src/transaction/user/input/DeleteEmancipatedSubnameWarning/DeleteEmancipatedSubnameWarning-flow.tsx similarity index 82% rename from src/transaction-flow/input/DeleteEmancipatedSubnameWarning/DeleteEmancipatedSubnameWarning-flow.tsx rename to src/transaction/user/input/DeleteEmancipatedSubnameWarning/DeleteEmancipatedSubnameWarning-flow.tsx index a305a3c90..cf70ccd85 100644 --- a/src/transaction-flow/input/DeleteEmancipatedSubnameWarning/DeleteEmancipatedSubnameWarning-flow.tsx +++ b/src/transaction/user/input/DeleteEmancipatedSubnameWarning/DeleteEmancipatedSubnameWarning-flow.tsx @@ -4,9 +4,8 @@ import styled, { css } from 'styled-components' import { Button, Dialog, mq } from '@ensdomains/thorin' import { useWrapperData } from '@app/hooks/ensjs/public/useWrapperData' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' -import { createTransactionItem } from '../../transaction/index' import { CenterAlignedTypography } from '../RevokePermissions/components/CenterAlignedTypography' const MessageContainer = styled(CenterAlignedTypography)(({ theme }) => [ @@ -27,7 +26,7 @@ export type Props = { data: Data } & TransactionDialogPassthrough -const DeleteEmancipatedSubnameWarning = ({ data, dispatch, onDismiss }: Props) => { +const DeleteEmancipatedSubnameWarning = ({ data, onDismiss, setTransactions, setStage }: Props) => { const { t } = useTranslation('transactionFlow') const { data: wrapperData, isLoading } = useWrapperData({ name: data.name }) @@ -41,17 +40,17 @@ const DeleteEmancipatedSubnameWarning = ({ data, dispatch, onDismiss }: Props) = const expiryLabel = expiryStr ? ` (${expiryStr})` : '' const handleDelete = () => { - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('deleteSubname', { + setTransactions([ + { + name: 'deleteSubname', + data: { name: data.name, contract: 'nameWrapper', method: 'setRecord', - }), - ], - }) - dispatch({ name: 'setFlowStage', payload: 'transaction' }) + }, + }, + ]) + setStage('transaction') } return ( diff --git a/src/transaction-flow/input/DeleteSubnameNotParentWarning/DeleteSubnameNotParentWarning-flow.tsx b/src/transaction/user/input/DeleteSubnameNotParentWarning/DeleteSubnameNotParentWarning-flow.tsx similarity index 83% rename from src/transaction-flow/input/DeleteSubnameNotParentWarning/DeleteSubnameNotParentWarning-flow.tsx rename to src/transaction/user/input/DeleteSubnameNotParentWarning/DeleteSubnameNotParentWarning-flow.tsx index 0a2b91ac0..3218135c2 100644 --- a/src/transaction-flow/input/DeleteSubnameNotParentWarning/DeleteSubnameNotParentWarning-flow.tsx +++ b/src/transaction/user/input/DeleteSubnameNotParentWarning/DeleteSubnameNotParentWarning-flow.tsx @@ -6,9 +6,8 @@ import { Button, Dialog } from '@ensdomains/thorin' import { usePrimaryNameOrAddress } from '@app/hooks/reverseRecord/usePrimaryNameOrAddress' import { useNameDetails } from '@app/hooks/useNameDetails' import { useOwners } from '@app/hooks/useOwners' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import TransactionLoader from '@app/transaction-flow/TransactionLoader' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' +import TransactionLoader from '@app/transaction/components/TransactionLoader' import { parentName } from '@app/utils/name' import { CenterAlignedTypography } from '../RevokePermissions/components/CenterAlignedTypography' @@ -22,7 +21,7 @@ export type Props = { data: Data } & TransactionDialogPassthrough -const DeleteSubnameNotParentWarning = ({ data, dispatch, onDismiss }: Props) => { +const DeleteSubnameNotParentWarning = ({ data, onDismiss, setTransactions, setStage }: Props) => { const { t } = useTranslation('transactionFlow') const { @@ -46,17 +45,17 @@ const DeleteSubnameNotParentWarning = ({ data, dispatch, onDismiss }: Props) => const isLoading = parentBasicLoading || parentPrimaryLoading const handleDelete = () => { - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('deleteSubname', { + setTransactions([ + { + name: 'deleteSubname', + data: { name: data.name, contract: data.contract, method: 'setRecord', - }), - ], - }) - dispatch({ name: 'setFlowStage', payload: 'transaction' }) + }, + }, + ]) + setStage('transaction') } if (isLoading) return diff --git a/src/transaction-flow/input/EditResolver/EditResolver-flow.tsx b/src/transaction/user/input/EditResolver/EditResolver-flow.tsx similarity index 80% rename from src/transaction-flow/input/EditResolver/EditResolver-flow.tsx rename to src/transaction/user/input/EditResolver/EditResolver-flow.tsx index 06da8274f..35d6ec74b 100644 --- a/src/transaction-flow/input/EditResolver/EditResolver-flow.tsx +++ b/src/transaction/user/input/EditResolver/EditResolver-flow.tsx @@ -8,9 +8,7 @@ import EditResolverForm from '@app/components/@molecules/EditResolver/EditResolv import { useIsWrapped } from '@app/hooks/useIsWrapped' import { useProfile } from '@app/hooks/useProfile' import useResolverEditor from '@app/hooks/useResolverEditor' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' - -import { createTransactionItem } from '../../transaction' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' type Data = { name: string @@ -20,7 +18,7 @@ export type Props = { data: Data } & TransactionDialogPassthrough -export const EditResolver = ({ data, dispatch, onDismiss }: Props) => { +export const EditResolver = ({ data, onDismiss, setTransactions, setStage }: Props) => { const { t } = useTranslation('transactionFlow') const { name } = data @@ -32,19 +30,19 @@ export const EditResolver = ({ data, dispatch, onDismiss }: Props) => { const handleCreateTransaction = useCallback( (newResolver: Address) => { - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('updateResolver', { + setTransactions([ + { + name: 'updateResolver', + data: { name, contract: isWrapped ? 'nameWrapper' : 'registry', resolverAddress: newResolver, - }), - ], - }) - dispatch({ name: 'setFlowStage', payload: 'transaction' }) + }, + }, + ]) + setStage('transaction') }, - [dispatch, name, isWrapped], + [setTransactions, setStage, name, isWrapped], ) const editResolverForm = useResolverEditor({ resolverAddress, callback: handleCreateTransaction }) diff --git a/src/transaction-flow/input/EditRoles/EditRoles-flow.tsx b/src/transaction/user/input/EditRoles/EditRoles-flow.tsx similarity index 82% rename from src/transaction-flow/input/EditRoles/EditRoles-flow.tsx rename to src/transaction/user/input/EditRoles/EditRoles-flow.tsx index 71c3e982b..df3dcef17 100644 --- a/src/transaction-flow/input/EditRoles/EditRoles-flow.tsx +++ b/src/transaction/user/input/EditRoles/EditRoles-flow.tsx @@ -8,10 +8,10 @@ import { useAccountSafely } from '@app/hooks/account/useAccountSafely' import useRoles, { Role, RoleRecord } from '@app/hooks/ownership/useRoles/useRoles' import { getAvailableRoles } from '@app/hooks/ownership/useRoles/utils/getAvailableRoles' import { useBasicName } from '@app/hooks/useBasicName' -import { createTransactionItem, TransactionItem } from '@app/transaction-flow/transaction' -import { makeTransferNameOrSubnameTransactionItem } from '@app/transaction-flow/transaction/utils/makeTransferNameOrSubnameTransactionItem' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' +import { createUserTransaction, type GenericUserTransaction } from '../../transaction' +import { makeTransferNameOrSubnameTransactionItem } from '../../transaction/utils/makeTransferNameOrSubnameTransactionItem' import { EditRoleView } from './views/EditRoleView/EditRoleView' import { MainView } from './views/MainView/MainView' @@ -27,7 +27,7 @@ export type Props = { data: Data } & TransactionDialogPassthrough -const EditRoles = ({ data: { name }, dispatch, onDismiss }: Props) => { +const EditRoles = ({ data: { name }, onDismiss, setTransactions, setStage }: Props) => { const [selectedRoleIndex, setSelectedRoleIndex] = useState(null) const roles = useRoles(name) @@ -73,7 +73,7 @@ const EditRoles = ({ data: { name }, dispatch, onDismiss }: Props) => { ) const transactions = [ dirtyValues['eth-record'] - ? createTransactionItem('updateEthAddress', { name, address: dirtyValues['eth-record'] }) + ? createUserTransaction('updateEthAddress', { name, address: dirtyValues['eth-record'] }) : null, dirtyValues.manager ? makeTransferNameOrSubnameTransactionItem({ @@ -97,20 +97,13 @@ const EditRoles = ({ data: { name }, dispatch, onDismiss }: Props) => { ( t, ): t is - | TransactionItem<'transferName'> - | TransactionItem<'transferSubname'> - | TransactionItem<'updateEthAddress'> => !!t, + | GenericUserTransaction<'transferName'> + | GenericUserTransaction<'transferSubname'> + | GenericUserTransaction<'updateEthAddress'> => !!t, ) - dispatch({ - name: 'setTransactions', - payload: transactions, - }) - - dispatch({ - name: 'setFlowStage', - payload: 'transaction', - }) + setTransactions(transactions) + setStage('transaction') } return ( diff --git a/src/transaction-flow/input/EditRoles/EditRoles.test.tsx b/src/transaction/user/input/EditRoles/EditRoles.test.tsx similarity index 98% rename from src/transaction-flow/input/EditRoles/EditRoles.test.tsx rename to src/transaction/user/input/EditRoles/EditRoles.test.tsx index 92209f244..829b85f5d 100644 --- a/src/transaction-flow/input/EditRoles/EditRoles.test.tsx +++ b/src/transaction/user/input/EditRoles/EditRoles.test.tsx @@ -67,7 +67,7 @@ vi.mock('@app/hooks/abilities/useAbilities', () => ({ })) let searchData: any[] = [] -vi.mock('@app/transaction-flow/input/EditRoles/hooks/useSimpleSearch.ts', () => ({ +vi.mock('@app/transaction/user/EditRoles/hooks/useSimpleSearch.ts', () => ({ useSimpleSearch: () => ({ mutate: (query: string) => { searchData = [{ name: `${query}.eth`, address: `0x${query}` }] diff --git a/src/transaction-flow/input/EditRoles/hooks/useSimpleSearch.ts b/src/transaction/user/input/EditRoles/hooks/useSimpleSearch.ts similarity index 100% rename from src/transaction-flow/input/EditRoles/hooks/useSimpleSearch.ts rename to src/transaction/user/input/EditRoles/hooks/useSimpleSearch.ts diff --git a/src/transaction-flow/input/EditRoles/views/EditRoleView/EditRoleView.tsx b/src/transaction/user/input/EditRoles/views/EditRoleView/EditRoleView.tsx similarity index 95% rename from src/transaction-flow/input/EditRoles/views/EditRoleView/EditRoleView.tsx rename to src/transaction/user/input/EditRoles/views/EditRoleView/EditRoleView.tsx index a021149f6..d95e41ba4 100644 --- a/src/transaction-flow/input/EditRoles/views/EditRoleView/EditRoleView.tsx +++ b/src/transaction/user/input/EditRoles/views/EditRoleView/EditRoleView.tsx @@ -7,9 +7,9 @@ import { match, P } from 'ts-pattern' import { Button, Dialog, Input, MagnifyingGlassSimpleSVG, mq } from '@ensdomains/thorin' import { DialogFooterWithBorder } from '@app/components/@molecules/DialogComponentVariants/DialogFooterWithBorder' -import { SearchViewErrorView } from '@app/transaction-flow/input/SendName/views/SearchView/views/SearchViewErrorView' -import { SearchViewLoadingView } from '@app/transaction-flow/input/SendName/views/SearchView/views/SearchViewLoadingView' -import { SearchViewNoResultsView } from '@app/transaction-flow/input/SendName/views/SearchView/views/SearchViewNoResultsView' +import { SearchViewErrorView } from '@app/transaction/user/input/SendName/views/SearchView/views/SearchViewErrorView' +import { SearchViewLoadingView } from '@app/transaction/user/input/SendName/views/SearchView/views/SearchViewLoadingView' +import { SearchViewNoResultsView } from '@app/transaction/user/input/SendName/views/SearchView/views/SearchViewNoResultsView' import type { EditRolesForm } from '../../EditRoles-flow' import { useSimpleSearch } from '../../hooks/useSimpleSearch' diff --git a/src/transaction-flow/input/EditRoles/views/EditRoleView/views/EditRoleIntroView.tsx b/src/transaction/user/input/EditRoles/views/EditRoleView/views/EditRoleIntroView.tsx similarity index 97% rename from src/transaction-flow/input/EditRoles/views/EditRoleView/views/EditRoleIntroView.tsx rename to src/transaction/user/input/EditRoles/views/EditRoleView/views/EditRoleIntroView.tsx index 546b64a01..fdb191e07 100644 --- a/src/transaction-flow/input/EditRoles/views/EditRoleView/views/EditRoleIntroView.tsx +++ b/src/transaction/user/input/EditRoles/views/EditRoleView/views/EditRoleIntroView.tsx @@ -7,7 +7,7 @@ import { Button, mq } from '@ensdomains/thorin' import { AvatarWithIdentifier } from '@app/components/@molecules/AvatarWithIdentifier/AvatarWithIdentifier' import { useAccountSafely } from '@app/hooks/account/useAccountSafely' import type { Role } from '@app/hooks/ownership/useRoles/useRoles' -import { SearchViewIntroView } from '@app/transaction-flow/input/SendName/views/SearchView/views/SearchViewIntroView' +import { SearchViewIntroView } from '@app/transaction/user/input/SendName/views/SearchView/views/SearchViewIntroView' import { emptyAddress } from '@app/utils/constants' const SHOW_REMOVE_ROLES: Role[] = ['eth-record'] diff --git a/src/transaction-flow/input/EditRoles/views/EditRoleView/views/EditRoleResultsView.tsx b/src/transaction/user/input/EditRoles/views/EditRoleView/views/EditRoleResultsView.tsx similarity index 94% rename from src/transaction-flow/input/EditRoles/views/EditRoleView/views/EditRoleResultsView.tsx rename to src/transaction/user/input/EditRoles/views/EditRoleView/views/EditRoleResultsView.tsx index 9eb358b09..d1056e632 100644 --- a/src/transaction-flow/input/EditRoles/views/EditRoleView/views/EditRoleResultsView.tsx +++ b/src/transaction/user/input/EditRoles/views/EditRoleView/views/EditRoleResultsView.tsx @@ -2,7 +2,7 @@ import styled, { css } from 'styled-components' import { Address } from 'viem' import { RoleRecord, type Role } from '@app/hooks/ownership/useRoles/useRoles' -import { SearchViewResult } from '@app/transaction-flow/input/SendName/views/SearchView/components/SearchViewResult' +import { SearchViewResult } from '@app/transaction/user/input/SendName/views/SearchView/components/SearchViewResult' import type { useSimpleSearch } from '../../../hooks/useSimpleSearch' diff --git a/src/transaction-flow/input/EditRoles/views/MainView/MainView.tsx b/src/transaction/user/input/EditRoles/views/MainView/MainView.tsx similarity index 100% rename from src/transaction-flow/input/EditRoles/views/MainView/MainView.tsx rename to src/transaction/user/input/EditRoles/views/MainView/MainView.tsx diff --git a/src/transaction-flow/input/EditRoles/views/MainView/components/NoneSetAvatarWithIdentifier.tsx b/src/transaction/user/input/EditRoles/views/MainView/components/NoneSetAvatarWithIdentifier.tsx similarity index 100% rename from src/transaction-flow/input/EditRoles/views/MainView/components/NoneSetAvatarWithIdentifier.tsx rename to src/transaction/user/input/EditRoles/views/MainView/components/NoneSetAvatarWithIdentifier.tsx diff --git a/src/transaction-flow/input/EditRoles/views/MainView/components/RoleCard.tsx b/src/transaction/user/input/EditRoles/views/MainView/components/RoleCard.tsx similarity index 100% rename from src/transaction-flow/input/EditRoles/views/MainView/components/RoleCard.tsx rename to src/transaction/user/input/EditRoles/views/MainView/components/RoleCard.tsx diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx b/src/transaction/user/input/ExtendNames/ExtendNames-flow.test.tsx similarity index 100% rename from src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx rename to src/transaction/user/input/ExtendNames/ExtendNames-flow.test.tsx diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx b/src/transaction/user/input/ExtendNames/ExtendNames-flow.tsx similarity index 95% rename from src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx rename to src/transaction/user/input/ExtendNames/ExtendNames-flow.tsx index 723d375d6..9ca491013 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx +++ b/src/transaction/user/input/ExtendNames/ExtendNames-flow.tsx @@ -10,6 +10,8 @@ import { Avatar, Button, CurrencyToggle, Dialog, Helper, Typography } from '@ens import { CacheableComponent } from '@app/components/@atoms/CacheableComponent' import { makeCurrencyDisplay } from '@app/components/@atoms/CurrencyText/CurrencyText' +import { ShortExpiry } from '@app/components/@atoms/ExpiryComponents/ExpiryComponents' +import GasDisplay from '@app/components/@atoms/GasDisplay' import { Invoice, InvoiceItem } from '@app/components/@atoms/Invoice/Invoice' import { PlusMinusControl } from '@app/components/@atoms/PlusMinusControl/PlusMinusControl' import { RegistrationTimeComparisonBanner } from '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner' @@ -20,16 +22,14 @@ import { useExpiry } from '@app/hooks/ensjs/public/useExpiry' import { usePrice } from '@app/hooks/ensjs/public/usePrice' import { useEthPrice } from '@app/hooks/useEthPrice' import { useZorb } from '@app/hooks/useZorb' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' import { CURRENCY_FLUCTUATION_BUFFER_PERCENTAGE } from '@app/utils/constants' import { ensAvatarConfig } from '@app/utils/query/ipfsGateway' import { ONE_DAY, ONE_YEAR, secondsToYears, yearsToSeconds } from '@app/utils/time' import useUserConfig from '@app/utils/useUserConfig' import { deriveYearlyFee, formatDurationOfDates } from '@app/utils/utils' -import { ShortExpiry } from '../../../components/@atoms/ExpiryComponents/ExpiryComponents' -import GasDisplay from '../../../components/@atoms/GasDisplay' +import { createUserTransaction } from '../../transaction' type View = 'name-list' | 'no-ownership-warning' | 'registration' @@ -170,7 +170,7 @@ export type Props = { const minSeconds = ONE_DAY -const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => { +const ExtendNames = ({ data: { names, isSelf }, onDismiss, setTransactions, setStage }: Props) => { const { t } = useTranslation(['transactionFlow', 'common']) const { data: ethPrice } = useEthPrice() @@ -219,7 +219,7 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const extendedDate = expiryDate ? new Date(expiryDate.getTime() + seconds * 1000) : undefined const transactions = [ - createTransactionItem('extendNames', { + createUserTransaction('extendNames', { names, duration: seconds, startDateTimestamp: expiryDate?.getTime(), @@ -302,8 +302,8 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => disabled: !!estimateGasLimitError, onClick: () => { if (!totalRentFee) return - dispatch({ name: 'setTransactions', payload: transactions }) - dispatch({ name: 'setFlowStage', payload: 'transaction' }) + setTransactions(transactions) + setStage('transaction') }, children: t('action.next', { ns: 'common' }), })) diff --git a/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx b/src/transaction/user/input/ProfileEditor/ProfileEditor-flow.tsx similarity index 91% rename from src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx rename to src/transaction/user/input/ProfileEditor/ProfileEditor-flow.tsx index 394f5e64c..b1bf8dcf7 100644 --- a/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx +++ b/src/transaction/user/input/ProfileEditor/ProfileEditor-flow.tsx @@ -10,25 +10,25 @@ import { Button, Dialog, mq, PlusSVG } from '@ensdomains/thorin' import { DisabledButtonWithTooltip } from '@app/components/@molecules/DisabledButtonWithTooltip' import { AvatarViewManager } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarViewManager' -import { AddProfileRecordView } from '@app/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView' -import { CustomProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput' -import { ProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput' -import { ProfileRecordTextarea } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea' +import { AddProfileRecordView } from '@app/components/pages/register/steps/Profile/AddProfileRecordView' +import { CustomProfileRecordInput } from '@app/components/pages/register/steps/Profile/CustomProfileRecordInput' +import { ProfileRecordInput } from '@app/components/pages/register/steps/Profile/ProfileRecordInput' +import { ProfileRecordTextarea } from '@app/components/pages/register/steps/Profile/ProfileRecordTextarea' import { getProfileRecordsDiff, isEthAddressRecord, profileEditorFormToProfileRecords, profileToProfileRecords, -} from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +} from '@app/components/pages/register/steps/Profile/profileRecordUtils' import { ProfileRecord } from '@app/constants/profileRecordOptions' import { useContractAddress } from '@app/hooks/chain/useContractAddress' import { useResolverStatus } from '@app/hooks/resolver/useResolverStatus' import { useIsWrapped } from '@app/hooks/useIsWrapped' import { useProfile } from '@app/hooks/useProfile' import { ProfileEditorForm, useProfileEditorForm } from '@app/hooks/useProfileEditorForm' -import { createTransactionItem, TransactionItem } from '@app/transaction-flow/transaction' -import TransactionLoader from '@app/transaction-flow/TransactionLoader' -import type { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' +import TransactionLoader from '@app/transaction/components/TransactionLoader' +import type { GenericStoredTransaction } from '@app/transaction/slices/createTransactionSlice' import { getResolverWrapperAwareness } from '@app/utils/utils' import ResolverWarningOverlay from './ResolverWarningOverlay' @@ -109,7 +109,13 @@ const SubmitButton = ({ ) } -const ProfileEditor = ({ data = {}, transactions = [], dispatch, onDismiss }: Props) => { +const ProfileEditor = ({ + data = {}, + transactions = [], + onDismiss, + setTransactions, + setStage, +}: Props) => { const { t } = useTranslation('register') const formRef = useRef(null) @@ -149,8 +155,9 @@ const ProfileEditor = ({ data = {}, transactions = [], dispatch, onDismiss }: Pr useEffect(() => { const updateProfileRecordsWithTransactionData = () => { const transaction = transactions.find( - (item: TransactionItem) => item.name === 'updateProfileRecords', - ) as TransactionItem<'updateProfileRecords'> + (item): item is GenericStoredTransaction<'updateProfileRecords'> => + item.name === 'updateProfileRecords', + ) if (!transaction) return const updatedRecords: ProfileRecord[] = transaction?.data?.records || [] updatedRecords.forEach((record) => { @@ -188,21 +195,21 @@ const ProfileEditor = ({ data = {}, transactions = [], dispatch, onDismiss }: Pr async (form: ProfileEditorForm) => { const records = profileEditorFormToProfileRecords(form) if (!profile?.resolverAddress) return - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('updateProfileRecords', { + setTransactions([ + { + name: 'updateProfileRecords', + data: { name, resolverAddress: profile.resolverAddress, records, previousRecords: existingRecords, clearRecords: false, - }), - ], - }) - dispatch({ name: 'setFlowStage', payload: 'transaction' }) + }, + }, + ]) + setStage('transaction') }, - [profile, name, existingRecords, dispatch], + [profile, name, existingRecords, setTransactions, setStage], ) const [avatarSrc, setAvatarSrc] = useState() @@ -385,8 +392,9 @@ const ProfileEditor = ({ data = {}, transactions = [], dispatch, onDismiss }: Pr hasMigratedProfile={resolverStatus.data?.hasMigratedProfile} latestResolverAddress={resolverAddress!} oldResolverAddress={profile?.resolverAddress!} - dispatch={dispatch} - onDismiss={() => dispatch({ name: 'stopFlow' })} + onDismiss={onDismiss} + setTransactions={setTransactions} + setStage={setStage} onDismissOverlay={() => setView('editor')} /> )) diff --git a/src/transaction-flow/input/ProfileEditor/ProfileEditor.test.tsx b/src/transaction/user/input/ProfileEditor/ProfileEditor.test.tsx similarity index 99% rename from src/transaction-flow/input/ProfileEditor/ProfileEditor.test.tsx rename to src/transaction/user/input/ProfileEditor/ProfileEditor.test.tsx index ed1a26542..f1a8c4303 100644 --- a/src/transaction-flow/input/ProfileEditor/ProfileEditor.test.tsx +++ b/src/transaction/user/input/ProfileEditor/ProfileEditor.test.tsx @@ -100,7 +100,7 @@ vi.mock('@app/utils/BreakpointProvider') vi.mock('@app/transaction-flow/TransactionFlowProvider') -vi.mock('@app/transaction-flow/input/ProfileEditor/components/ProfileBlurb', () => ({ +vi.mock('@app/transaction/user/ProfileEditor/components/ProfileBlurb', () => ({ ProfileBlurb: () =>
Profile Blurb
, })) diff --git a/src/transaction-flow/input/ProfileEditor/ResolverWarningOverlay.tsx b/src/transaction/user/input/ProfileEditor/ResolverWarningOverlay.tsx similarity index 79% rename from src/transaction-flow/input/ProfileEditor/ResolverWarningOverlay.tsx rename to src/transaction/user/input/ProfileEditor/ResolverWarningOverlay.tsx index 1684bbf29..1c8f5091d 100644 --- a/src/transaction-flow/input/ProfileEditor/ResolverWarningOverlay.tsx +++ b/src/transaction/user/input/ProfileEditor/ResolverWarningOverlay.tsx @@ -3,9 +3,8 @@ import { useTranslation } from 'react-i18next' import { Address } from 'viem' import { useResolverStatus } from '@app/hooks/resolver/useResolverStatus' -import { makeIntroItem } from '@app/transaction-flow/intro' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' +import { useTransactionManager } from '@app/transaction/transactionManager' import { InvalidResolverView } from './views/InvalidResolverView' import { MigrateProfileSelectorView } from './views/MigrateProfileSelectorView.tsx' @@ -54,12 +53,14 @@ const ResolverWarningOverlay = ({ hasOldRegistry = false, latestResolverAddress, oldResolverAddress, - dispatch, onDismiss, onDismissOverlay, + setStage, + setTransactions, }: Props) => { const { t } = useTranslation('transactionFlow') const [selectedProfile, setSelectedProfile] = useState('latest') + const startFlow = useTransactionManager((s) => s.startFlow) const flow: View[] = useMemo(() => { if (hasOldRegistry) return ['migrateRegistry'] @@ -101,99 +102,111 @@ const ResolverWarningOverlay = ({ } const handleUpdateResolver = () => { - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('updateResolver', { + setTransactions([ + { + name: 'updateResolver', + data: { name, contract: isWrapped ? 'nameWrapper' : 'registry', resolverAddress: latestResolverAddress, - }), - ], - }) - dispatch({ - name: 'setFlowStage', - payload: 'transaction', - }) + }, + }, + ]) + setStage('transaction') } const handleMigrateProfile = () => { - dispatch({ - name: 'startFlow', - key: `migrate-profile-${name}`, - payload: { - intro: { - title: ['input.profileEditor.intro.migrateProfile.title', { ns: 'transactionFlow' }], - content: makeIntroItem('GenericWithDescription', { + startFlow({ + flowId: `migrate-profile-${name}`, + intro: { + title: ['input.profileEditor.intro.migrateProfile.title', { ns: 'transactionFlow' }], + content: { + name: 'GenericWithDescription', + data: { description: t('input.profileEditor.intro.migrateProfile.description'), - }), + }, }, - transactions: [ - createTransactionItem('migrateProfile', { + }, + transactions: [ + { + name: 'migrateProfile', + data: { name, - }), - createTransactionItem('updateResolver', { + }, + }, + { + name: 'updateResolver', + data: { name, contract: isWrapped ? 'nameWrapper' : 'registry', resolverAddress: latestResolverAddress, - }), - ], - }, + }, + }, + ], }) } const handleResetProfile = () => { - dispatch({ - name: 'startFlow', - key: `reset-profile-${name}`, - payload: { - intro: { - title: ['input.profileEditor.intro.resetProfile.title', { ns: 'transactionFlow' }], - content: makeIntroItem('GenericWithDescription', { + startFlow({ + flowId: `reset-profile-${name}`, + intro: { + title: ['input.profileEditor.intro.resetProfile.title', { ns: 'transactionFlow' }], + content: { + name: 'GenericWithDescription', + data: { description: t('input.profileEditor.intro.resetProfile.description'), - }), + }, }, - transactions: [ - createTransactionItem('resetProfile', { + }, + transactions: [ + { + name: 'resetProfile', + data: { name, resolverAddress: latestResolverAddress, - }), - createTransactionItem('updateResolver', { + }, + }, + { + name: 'updateResolver', + data: { name, contract: isWrapped ? 'nameWrapper' : 'registry', resolverAddress: latestResolverAddress, - }), - ], - }, + }, + }, + ], }) } const handleMigrateCurrentProfileToLatest = async () => { - dispatch({ - name: 'startFlow', - key: `migrate-profile-with-reset-${name}`, - payload: { - intro: { - title: [ - 'input.profileEditor.intro.migrateCurrentProfile.title', - { ns: 'transactionFlow' }, - ], - content: makeIntroItem('GenericWithDescription', { + startFlow({ + flowId: `migrate-profile-with-reset-${name}`, + intro: { + title: ['input.profileEditor.intro.migrateCurrentProfile.title', { ns: 'transactionFlow' }], + content: { + name: 'GenericWithDescription', + data: { description: t('input.profileEditor.intro.migrateCurrentProfile.description'), - }), + }, }, - transactions: [ - createTransactionItem('migrateProfileWithReset', { + }, + transactions: [ + { + name: 'migrateProfileWithReset', + data: { name, resolverAddress: oldResolverAddress, - }), - createTransactionItem('updateResolver', { + }, + }, + { + name: 'updateResolver', + data: { name, contract: isWrapped ? 'nameWrapper' : 'registry', resolverAddress: latestResolverAddress, - }), - ], - }, + }, + }, + ], }) } diff --git a/src/transaction-flow/input/ProfileEditor/WrappedAvatarButton.tsx b/src/transaction/user/input/ProfileEditor/WrappedAvatarButton.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/WrappedAvatarButton.tsx rename to src/transaction/user/input/ProfileEditor/WrappedAvatarButton.tsx diff --git a/src/transaction-flow/input/ProfileEditor/components/CenteredTypography.tsx b/src/transaction/user/input/ProfileEditor/components/CenteredTypography.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/components/CenteredTypography.tsx rename to src/transaction/user/input/ProfileEditor/components/CenteredTypography.tsx diff --git a/src/transaction-flow/input/ProfileEditor/components/ContentContainer.tsx b/src/transaction/user/input/ProfileEditor/components/ContentContainer.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/components/ContentContainer.tsx rename to src/transaction/user/input/ProfileEditor/components/ContentContainer.tsx diff --git a/src/transaction-flow/input/ProfileEditor/components/DetailedSwitch.tsx b/src/transaction/user/input/ProfileEditor/components/DetailedSwitch.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/components/DetailedSwitch.tsx rename to src/transaction/user/input/ProfileEditor/components/DetailedSwitch.tsx diff --git a/src/transaction-flow/input/ProfileEditor/components/ProfileBlurb.tsx b/src/transaction/user/input/ProfileEditor/components/ProfileBlurb.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/components/ProfileBlurb.tsx rename to src/transaction/user/input/ProfileEditor/components/ProfileBlurb.tsx diff --git a/src/transaction-flow/input/ProfileEditor/components/SkipButton.tsx b/src/transaction/user/input/ProfileEditor/components/SkipButton.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/components/SkipButton.tsx rename to src/transaction/user/input/ProfileEditor/components/SkipButton.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/InvalidResolverView.tsx b/src/transaction/user/input/ProfileEditor/views/InvalidResolverView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/InvalidResolverView.tsx rename to src/transaction/user/input/ProfileEditor/views/InvalidResolverView.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/MigrateProfileSelectorView.tsx.tsx b/src/transaction/user/input/ProfileEditor/views/MigrateProfileSelectorView.tsx.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/MigrateProfileSelectorView.tsx.tsx rename to src/transaction/user/input/ProfileEditor/views/MigrateProfileSelectorView.tsx.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/MigrateProfileWarningView.tsx b/src/transaction/user/input/ProfileEditor/views/MigrateProfileWarningView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/MigrateProfileWarningView.tsx rename to src/transaction/user/input/ProfileEditor/views/MigrateProfileWarningView.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/MigrateRegistryView.tsx b/src/transaction/user/input/ProfileEditor/views/MigrateRegistryView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/MigrateRegistryView.tsx rename to src/transaction/user/input/ProfileEditor/views/MigrateRegistryView.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/NoResolverView.tsx b/src/transaction/user/input/ProfileEditor/views/NoResolverView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/NoResolverView.tsx rename to src/transaction/user/input/ProfileEditor/views/NoResolverView.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/ResetProfileView.tsx b/src/transaction/user/input/ProfileEditor/views/ResetProfileView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/ResetProfileView.tsx rename to src/transaction/user/input/ProfileEditor/views/ResetProfileView.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/ResolverNotNameWrapperAwareView.tsx b/src/transaction/user/input/ProfileEditor/views/ResolverNotNameWrapperAwareView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/ResolverNotNameWrapperAwareView.tsx rename to src/transaction/user/input/ProfileEditor/views/ResolverNotNameWrapperAwareView.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/ResolverOutOfDateView.tsx b/src/transaction/user/input/ProfileEditor/views/ResolverOutOfDateView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/ResolverOutOfDateView.tsx rename to src/transaction/user/input/ProfileEditor/views/ResolverOutOfDateView.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/ResolverOutOfSyncView.tsx b/src/transaction/user/input/ProfileEditor/views/ResolverOutOfSyncView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/ResolverOutOfSyncView.tsx rename to src/transaction/user/input/ProfileEditor/views/ResolverOutOfSyncView.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/TransferOrResetProfileView.tsx b/src/transaction/user/input/ProfileEditor/views/TransferOrResetProfileView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/TransferOrResetProfileView.tsx rename to src/transaction/user/input/ProfileEditor/views/TransferOrResetProfileView.tsx diff --git a/src/transaction-flow/input/ProfileEditor/views/UpdateResolverOrResetProfileView.tsx b/src/transaction/user/input/ProfileEditor/views/UpdateResolverOrResetProfileView.tsx similarity index 100% rename from src/transaction-flow/input/ProfileEditor/views/UpdateResolverOrResetProfileView.tsx rename to src/transaction/user/input/ProfileEditor/views/UpdateResolverOrResetProfileView.tsx diff --git a/src/transaction-flow/input/ResetPrimaryName/ResetPrimaryName-flow.tsx b/src/transaction/user/input/ResetPrimaryName/ResetPrimaryName-flow.tsx similarity index 71% rename from src/transaction-flow/input/ResetPrimaryName/ResetPrimaryName-flow.tsx rename to src/transaction/user/input/ResetPrimaryName/ResetPrimaryName-flow.tsx index d9aa797c9..285e22566 100644 --- a/src/transaction-flow/input/ResetPrimaryName/ResetPrimaryName-flow.tsx +++ b/src/transaction/user/input/ResetPrimaryName/ResetPrimaryName-flow.tsx @@ -3,8 +3,8 @@ import type { Address } from 'viem' import { Button, Dialog } from '@ensdomains/thorin' -import { createTransactionItem } from '../../transaction' -import { TransactionDialogPassthrough } from '../../types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' + import { CenteredTypography } from '../ProfileEditor/components/CenteredTypography' type Data = { @@ -16,22 +16,17 @@ export type Props = { data: Data } & TransactionDialogPassthrough -const ResetPrimaryName = ({ data: { address }, dispatch, onDismiss }: Props) => { +const ResetPrimaryName = ({ data: { address }, setTransactions, setStage, onDismiss }: Props) => { const { t } = useTranslation('transactionFlow') const handleSubmit = async () => { - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('resetPrimaryName', { - address, - }), - ], - }) - dispatch({ - name: 'setFlowStage', - payload: 'transaction', - }) + setTransactions([ + { + name: 'resetPrimaryName', + data: { address }, + }, + ]) + setStage('transaction') } return ( diff --git a/src/transaction-flow/input/RevokePermissions/RevokePermissions-flow.tsx b/src/transaction/user/input/RevokePermissions/RevokePermissions-flow.tsx similarity index 93% rename from src/transaction-flow/input/RevokePermissions/RevokePermissions-flow.tsx rename to src/transaction/user/input/RevokePermissions/RevokePermissions-flow.tsx index 9e79b1b34..3e42cc9ab 100644 --- a/src/transaction-flow/input/RevokePermissions/RevokePermissions-flow.tsx +++ b/src/transaction/user/input/RevokePermissions/RevokePermissions-flow.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, Dispatch, useMemo, useRef, useState } from 'react' +import { ComponentProps, useMemo, useRef, useState } from 'react' import { useForm, useWatch } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { match } from 'ts-pattern' @@ -13,9 +13,8 @@ import { import { Button, Dialog } from '@ensdomains/thorin' import { useExpiry } from '@app/hooks/ensjs/public/useExpiry' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import type changePermissions from '@app/transaction-flow/transaction/changePermissions' -import { TransactionDialogPassthrough, TransactionFlowAction } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' +import type changePermissions from '@app/transaction/user/transaction/changePermissions' import { ExtractTransactionData } from '@app/types' import { dateTimeLocalToDate, dateToDateTimeLocal } from '@app/utils/datetime-local' @@ -78,8 +77,6 @@ export type RevokePermissionsDialogContentProps = ComponentProps void - dispatch: Dispatch } & TransactionDialogPassthrough export type View = @@ -178,7 +175,7 @@ const getIntialValueForCurrentIndex = (flow: View[], transactionData?: Transacti return flow.length - 1 } -const RevokePermissions = ({ data, transactions, onDismiss, dispatch }: Props) => { +const RevokePermissions = ({ data, transactions, onDismiss, setTransactions, setStage }: Props) => { const { name, flowType, @@ -279,10 +276,10 @@ const RevokePermissions = ({ data, transactions, onDismiss, dispatch }: Props) = ? Math.floor(dateTimeLocalToDate(form.expiryCustom).getTime() / 1000) : undefined - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('changePermissions', { + setTransactions([ + { + name: 'changePermissions', + data: { name, contract: 'setChildFuses', fuses: { @@ -290,23 +287,22 @@ const RevokePermissions = ({ data, transactions, onDismiss, dispatch }: Props) = child: childNamedFuses, }, expiry: form.expiryType === 'max' ? maxExpiry : customExpiry, - }), - ], - }) + }, + }, + ]) } else { - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('changePermissions', { + setTransactions([ + { + name: 'changePermissions', + data: { name, contract: 'setFuses', fuses: childNamedFuses, - }), - ], - }) + }, + }, + ]) } - - dispatch({ name: 'setFlowStage', payload: 'transaction' }) + setStage('transaction') } const [isDisabled, setDisabled] = useState(true) diff --git a/src/transaction-flow/input/RevokePermissions/RevokePermissions.test.tsx b/src/transaction/user/input/RevokePermissions/RevokePermissions.test.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/RevokePermissions.test.tsx rename to src/transaction/user/input/RevokePermissions/RevokePermissions.test.tsx diff --git a/src/transaction-flow/input/RevokePermissions/components/CenterAlignedTypography.tsx b/src/transaction/user/input/RevokePermissions/components/CenterAlignedTypography.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/components/CenterAlignedTypography.tsx rename to src/transaction/user/input/RevokePermissions/components/CenterAlignedTypography.tsx diff --git a/src/transaction-flow/input/RevokePermissions/components/ControlledNextButton.tsx b/src/transaction/user/input/RevokePermissions/components/ControlledNextButton.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/components/ControlledNextButton.tsx rename to src/transaction/user/input/RevokePermissions/components/ControlledNextButton.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/GrantExtendExpiryView.tsx b/src/transaction/user/input/RevokePermissions/views/GrantExtendExpiryView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/GrantExtendExpiryView.tsx rename to src/transaction/user/input/RevokePermissions/views/GrantExtendExpiryView.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/NameConfirmationWarningView.test.tsx b/src/transaction/user/input/RevokePermissions/views/NameConfirmationWarningView.test.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/NameConfirmationWarningView.test.tsx rename to src/transaction/user/input/RevokePermissions/views/NameConfirmationWarningView.test.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/NameConfirmationWarningView.tsx b/src/transaction/user/input/RevokePermissions/views/NameConfirmationWarningView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/NameConfirmationWarningView.tsx rename to src/transaction/user/input/RevokePermissions/views/NameConfirmationWarningView.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/ParentRevokePermissionsView.tsx b/src/transaction/user/input/RevokePermissions/views/ParentRevokePermissionsView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/ParentRevokePermissionsView.tsx rename to src/transaction/user/input/RevokePermissions/views/ParentRevokePermissionsView.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/RevokeChangeFusesView.tsx b/src/transaction/user/input/RevokePermissions/views/RevokeChangeFusesView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/RevokeChangeFusesView.tsx rename to src/transaction/user/input/RevokePermissions/views/RevokeChangeFusesView.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/RevokeChangeFusesWarningView.tsx b/src/transaction/user/input/RevokePermissions/views/RevokeChangeFusesWarningView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/RevokeChangeFusesWarningView.tsx rename to src/transaction/user/input/RevokePermissions/views/RevokeChangeFusesWarningView.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/RevokePCCView.tsx b/src/transaction/user/input/RevokePermissions/views/RevokePCCView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/RevokePCCView.tsx rename to src/transaction/user/input/RevokePermissions/views/RevokePCCView.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/RevokePermissionsView.tsx b/src/transaction/user/input/RevokePermissions/views/RevokePermissionsView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/RevokePermissionsView.tsx rename to src/transaction/user/input/RevokePermissions/views/RevokePermissionsView.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/RevokeUnwrapView.tsx b/src/transaction/user/input/RevokePermissions/views/RevokeUnwrapView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/RevokeUnwrapView.tsx rename to src/transaction/user/input/RevokePermissions/views/RevokeUnwrapView.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/RevokeWarningView.tsx b/src/transaction/user/input/RevokePermissions/views/RevokeWarningView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/RevokeWarningView.tsx rename to src/transaction/user/input/RevokePermissions/views/RevokeWarningView.tsx diff --git a/src/transaction-flow/input/RevokePermissions/views/SetExpiryView.tsx b/src/transaction/user/input/RevokePermissions/views/SetExpiryView.tsx similarity index 100% rename from src/transaction-flow/input/RevokePermissions/views/SetExpiryView.tsx rename to src/transaction/user/input/RevokePermissions/views/SetExpiryView.tsx diff --git a/src/transaction-flow/input/SelectPrimaryName/SelectPrimaryName-flow.tsx b/src/transaction/user/input/SelectPrimaryName/SelectPrimaryName-flow.tsx similarity index 95% rename from src/transaction-flow/input/SelectPrimaryName/SelectPrimaryName-flow.tsx rename to src/transaction/user/input/SelectPrimaryName/SelectPrimaryName-flow.tsx index 3eda7ea98..0f0b5fe98 100644 --- a/src/transaction-flow/input/SelectPrimaryName/SelectPrimaryName-flow.tsx +++ b/src/transaction/user/input/SelectPrimaryName/SelectPrimaryName-flow.tsx @@ -26,12 +26,13 @@ import useDebouncedCallback from '@app/hooks/useDebouncedCallback' import { useIsWrapped } from '@app/hooks/useIsWrapped' import { useProfile } from '@app/hooks/useProfile' import { createQueryKey } from '@app/hooks/useQueryOptions' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' +import { useTransactionManager } from '@app/transaction/transactionManager' import { nameToFormData, UnknownLabelsForm, FormData as UnknownLabelsFormData, -} from '@app/transaction-flow/input/UnknownLabels/views/UnknownLabelsForm' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' +} from '@app/transaction/user/input/UnknownLabels/views/UnknownLabelsForm' import { TaggedNameItemWithFuseCheck } from './components/TaggedNameItemWithFuseCheck' @@ -107,10 +108,11 @@ const ErrorContainer = styled.div( `, ) -const SelectPrimaryName = ({ data: { address }, dispatch, onDismiss }: Props) => { +const SelectPrimaryName = ({ data: { address }, onDismiss, setTransactions, setStage }: Props) => { const { t } = useTranslation('transactionFlow') const formRef = useRef(null) const queryClient = useQueryClient() + const startFlow = useTransactionManager((s) => s.startFlow) const form = useForm({ mode: 'onChange', @@ -191,20 +193,13 @@ const SelectPrimaryName = ({ data: { address }, dispatch, onDismiss }: Props) => const transactionCount = transactionFlowItem.transactions.length if (transactionCount === 1) { // TODO: Fix typescript transactions error - dispatch({ - name: 'setTransactions', - payload: transactionFlowItem.transactions as any[], - }) - dispatch({ - name: 'setFlowStage', - payload: 'transaction', - }) + setTransactions(transactionFlowItem.transactions) + setStage('transaction') return } - dispatch({ - name: 'startFlow', - key: 'ChangePrimaryName', - payload: transactionFlowItem, + startFlow({ + flowId: 'ChangePrimaryName', + ...transactionFlowItem, }) } diff --git a/src/transaction-flow/input/SelectPrimaryName/SelectPrimaryName.test.tsx b/src/transaction/user/input/SelectPrimaryName/SelectPrimaryName.test.tsx similarity index 100% rename from src/transaction-flow/input/SelectPrimaryName/SelectPrimaryName.test.tsx rename to src/transaction/user/input/SelectPrimaryName/SelectPrimaryName.test.tsx diff --git a/src/transaction-flow/input/SelectPrimaryName/components/TaggedNameItemWithFuseCheck.test.tsx b/src/transaction/user/input/SelectPrimaryName/components/TaggedNameItemWithFuseCheck.test.tsx similarity index 100% rename from src/transaction-flow/input/SelectPrimaryName/components/TaggedNameItemWithFuseCheck.test.tsx rename to src/transaction/user/input/SelectPrimaryName/components/TaggedNameItemWithFuseCheck.test.tsx diff --git a/src/transaction-flow/input/SelectPrimaryName/components/TaggedNameItemWithFuseCheck.tsx b/src/transaction/user/input/SelectPrimaryName/components/TaggedNameItemWithFuseCheck.tsx similarity index 100% rename from src/transaction-flow/input/SelectPrimaryName/components/TaggedNameItemWithFuseCheck.tsx rename to src/transaction/user/input/SelectPrimaryName/components/TaggedNameItemWithFuseCheck.tsx diff --git a/src/transaction-flow/input/SendName/SendName-flow.tsx b/src/transaction/user/input/SendName/SendName-flow.tsx similarity index 93% rename from src/transaction-flow/input/SendName/SendName-flow.tsx rename to src/transaction/user/input/SendName/SendName-flow.tsx index d8b4372ae..9b82f8a7d 100644 --- a/src/transaction-flow/input/SendName/SendName-flow.tsx +++ b/src/transaction/user/input/SendName/SendName-flow.tsx @@ -10,7 +10,7 @@ import { useNameType } from '@app/hooks/nameType/useNameType' import useRoles from '@app/hooks/ownership/useRoles/useRoles' import { useBasicName } from '@app/hooks/useBasicName' import { useResolverHasInterfaces } from '@app/hooks/useResolverHasInterfaces' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' import { checkCanSend, senderRole } from './utils/checkCanSend' import { getSendNameTransactions } from './utils/getSendNameTransactions' @@ -38,7 +38,7 @@ export type Props = { data: Data } & TransactionDialogPassthrough -const SendName = ({ data: { name }, dispatch, onDismiss }: Props) => { +const SendName = ({ data: { name }, onDismiss, setStage, setTransactions }: Props) => { const account = useAccountSafely() const abilities = useAbilities({ name }) const nameType = useNameType(name) @@ -107,15 +107,8 @@ const SendName = ({ data: { name }, dispatch, onDismiss }: Props) => { if (_transactions.length === 0) return - dispatch({ - name: 'setTransactions', - payload: _transactions, - }) - - dispatch({ - name: 'setFlowStage', - payload: 'transaction', - }) + setTransactions(_transactions) + setStage('transaction') } const canSend = checkCanSend({ abilities: abilities.data, nameType: nameType.data }) diff --git a/src/transaction-flow/input/SendName/SendName.test.tsx b/src/transaction/user/input/SendName/SendName.test.tsx similarity index 97% rename from src/transaction-flow/input/SendName/SendName.test.tsx rename to src/transaction/user/input/SendName/SendName.test.tsx index ad7703403..440df5c77 100644 --- a/src/transaction-flow/input/SendName/SendName.test.tsx +++ b/src/transaction/user/input/SendName/SendName.test.tsx @@ -66,7 +66,7 @@ vi.mock('@app/hooks/abilities/useAbilities', () => ({ })) let searchData: any[] = [] -vi.mock('@app/transaction-flow/input/EditRoles/hooks/useSimpleSearch.ts', () => ({ +vi.mock('@app/transaction/user/EditRoles/hooks/useSimpleSearch.ts', () => ({ useSimpleSearch: () => ({ mutate: (query: string) => { searchData = [{ name: `${query}.eth`, address: `0x${query}` }] diff --git a/src/transaction-flow/input/SendName/utils/checkCanSend.ts b/src/transaction/user/input/SendName/utils/checkCanSend.ts similarity index 100% rename from src/transaction-flow/input/SendName/utils/checkCanSend.ts rename to src/transaction/user/input/SendName/utils/checkCanSend.ts diff --git a/src/transaction-flow/input/SendName/utils/getSendNameTransactions.test.ts b/src/transaction/user/input/SendName/utils/getSendNameTransactions.test.ts similarity index 100% rename from src/transaction-flow/input/SendName/utils/getSendNameTransactions.test.ts rename to src/transaction/user/input/SendName/utils/getSendNameTransactions.test.ts diff --git a/src/transaction-flow/input/SendName/utils/getSendNameTransactions.ts b/src/transaction/user/input/SendName/utils/getSendNameTransactions.ts similarity index 77% rename from src/transaction-flow/input/SendName/utils/getSendNameTransactions.ts rename to src/transaction/user/input/SendName/utils/getSendNameTransactions.ts index b721efa9c..e8013197d 100644 --- a/src/transaction-flow/input/SendName/utils/getSendNameTransactions.ts +++ b/src/transaction/user/input/SendName/utils/getSendNameTransactions.ts @@ -1,8 +1,11 @@ import { Address } from 'viem' import type { useAbilities } from '@app/hooks/abilities/useAbilities' -import { createTransactionItem, TransactionItem } from '@app/transaction-flow/transaction' -import { makeTransferNameOrSubnameTransactionItem } from '@app/transaction-flow/transaction/utils/makeTransferNameOrSubnameTransactionItem' +import { + createUserTransaction, + type GenericUserTransaction, +} from '@app/transaction/user/transaction' +import { makeTransferNameOrSubnameTransactionItem } from '@app/transaction/user/transaction/utils/makeTransferNameOrSubnameTransactionItem' import type { SendNameForm } from '../SendName-flow' @@ -29,10 +32,10 @@ export const getSendNameTransactions = ({ const _transactions = [ setEthRecordOnly - ? createTransactionItem('updateEthAddress', { name, address: recipient }) + ? createUserTransaction('updateEthAddress', { name, address: recipient }) : null, setEthRecordAndResetProfile && resolverAddress - ? createTransactionItem('resetProfileWithRecords', { + ? createUserTransaction('resetProfileWithRecords', { name, records: { coins: [{ coin: 'ETH', value: recipient }], @@ -62,10 +65,10 @@ export const getSendNameTransactions = ({ ( transaction, ): transaction is - | TransactionItem<'transferName'> - | TransactionItem<'transferSubname'> - | TransactionItem<'updateEthAddress'> - | TransactionItem<'resetProfileWithRecords'> => !!transaction, + | GenericUserTransaction<'transferName'> + | GenericUserTransaction<'transferSubname'> + | GenericUserTransaction<'updateEthAddress'> + | GenericUserTransaction<'resetProfileWithRecords'> => !!transaction, ) return _transactions as NonNullable<(typeof _transactions)[number]>[] diff --git a/src/transaction-flow/input/SendName/views/CannotSendView.tsx b/src/transaction/user/input/SendName/views/CannotSendView.tsx similarity index 100% rename from src/transaction-flow/input/SendName/views/CannotSendView.tsx rename to src/transaction/user/input/SendName/views/CannotSendView.tsx diff --git a/src/transaction-flow/input/SendName/views/ConfirmationView.tsx b/src/transaction/user/input/SendName/views/ConfirmationView.tsx similarity index 100% rename from src/transaction-flow/input/SendName/views/ConfirmationView.tsx rename to src/transaction/user/input/SendName/views/ConfirmationView.tsx diff --git a/src/transaction-flow/input/SendName/views/SearchView/SearchView.tsx b/src/transaction/user/input/SendName/views/SearchView/SearchView.tsx similarity index 98% rename from src/transaction-flow/input/SendName/views/SearchView/SearchView.tsx rename to src/transaction/user/input/SendName/views/SearchView/SearchView.tsx index f56d37850..425c5e895 100644 --- a/src/transaction-flow/input/SendName/views/SearchView/SearchView.tsx +++ b/src/transaction/user/input/SendName/views/SearchView/SearchView.tsx @@ -8,7 +8,7 @@ import { Button, Dialog, MagnifyingGlassSimpleSVG } from '@ensdomains/thorin' import { DialogFooterWithBorder } from '@app/components/@molecules/DialogComponentVariants/DialogFooterWithBorder' import { DialogInput } from '@app/components/@molecules/DialogComponentVariants/DialogInput' -import { useSimpleSearch } from '@app/transaction-flow/input/EditRoles/hooks/useSimpleSearch' +import { useSimpleSearch } from '@app/transaction/user/input/EditRoles/hooks/useSimpleSearch' import type { SendNameForm } from '../../SendName-flow' import { SearchViewErrorView } from './views/SearchViewErrorView' diff --git a/src/transaction-flow/input/SendName/views/SearchView/components/SearchViewResult.tsx b/src/transaction/user/input/SendName/views/SearchView/components/SearchViewResult.tsx similarity index 100% rename from src/transaction-flow/input/SendName/views/SearchView/components/SearchViewResult.tsx rename to src/transaction/user/input/SendName/views/SearchView/components/SearchViewResult.tsx diff --git a/src/transaction-flow/input/SendName/views/SearchView/views/SearchViewErrorView.tsx b/src/transaction/user/input/SendName/views/SearchView/views/SearchViewErrorView.tsx similarity index 100% rename from src/transaction-flow/input/SendName/views/SearchView/views/SearchViewErrorView.tsx rename to src/transaction/user/input/SendName/views/SearchView/views/SearchViewErrorView.tsx diff --git a/src/transaction-flow/input/SendName/views/SearchView/views/SearchViewIntroView.tsx b/src/transaction/user/input/SendName/views/SearchView/views/SearchViewIntroView.tsx similarity index 100% rename from src/transaction-flow/input/SendName/views/SearchView/views/SearchViewIntroView.tsx rename to src/transaction/user/input/SendName/views/SearchView/views/SearchViewIntroView.tsx diff --git a/src/transaction-flow/input/SendName/views/SearchView/views/SearchViewLoadingView.tsx b/src/transaction/user/input/SendName/views/SearchView/views/SearchViewLoadingView.tsx similarity index 100% rename from src/transaction-flow/input/SendName/views/SearchView/views/SearchViewLoadingView.tsx rename to src/transaction/user/input/SendName/views/SearchView/views/SearchViewLoadingView.tsx diff --git a/src/transaction-flow/input/SendName/views/SearchView/views/SearchViewNoResultsView.tsx b/src/transaction/user/input/SendName/views/SearchView/views/SearchViewNoResultsView.tsx similarity index 100% rename from src/transaction-flow/input/SendName/views/SearchView/views/SearchViewNoResultsView.tsx rename to src/transaction/user/input/SendName/views/SearchView/views/SearchViewNoResultsView.tsx diff --git a/src/transaction-flow/input/SendName/views/SearchView/views/SearchViewResultsView.tsx b/src/transaction/user/input/SendName/views/SearchView/views/SearchViewResultsView.tsx similarity index 100% rename from src/transaction-flow/input/SendName/views/SearchView/views/SearchViewResultsView.tsx rename to src/transaction/user/input/SendName/views/SearchView/views/SearchViewResultsView.tsx diff --git a/src/transaction-flow/input/SendName/views/SummaryView/SummaryView.tsx b/src/transaction/user/input/SendName/views/SummaryView/SummaryView.tsx similarity index 97% rename from src/transaction-flow/input/SendName/views/SummaryView/SummaryView.tsx rename to src/transaction/user/input/SendName/views/SummaryView/SummaryView.tsx index d848fe0f2..2453a37d3 100644 --- a/src/transaction-flow/input/SendName/views/SummaryView/SummaryView.tsx +++ b/src/transaction/user/input/SendName/views/SummaryView/SummaryView.tsx @@ -6,7 +6,7 @@ import { Button, Dialog, Field } from '@ensdomains/thorin' import { AvatarWithIdentifier } from '@app/components/@molecules/AvatarWithIdentifier/AvatarWithIdentifier' import { useExpiry } from '@app/hooks/ensjs/public/useExpiry' -import TransactionLoader from '@app/transaction-flow/TransactionLoader' +import TransactionLoader from '@app/transaction/components/TransactionLoader' import { DetailedSwitch } from '../../../ProfileEditor/components/DetailedSwitch' import type { SendNameForm } from '../../SendName-flow' diff --git a/src/transaction-flow/input/SendName/views/SummaryView/components/SummarySection.tsx b/src/transaction/user/input/SendName/views/SummaryView/components/SummarySection.tsx similarity index 100% rename from src/transaction-flow/input/SendName/views/SummaryView/components/SummarySection.tsx rename to src/transaction/user/input/SendName/views/SummaryView/components/SummarySection.tsx diff --git a/src/transaction-flow/input/SyncManager/SyncManager-flow.tsx b/src/transaction/user/input/SyncManager/SyncManager-flow.tsx similarity index 78% rename from src/transaction-flow/input/SyncManager/SyncManager-flow.tsx rename to src/transaction/user/input/SyncManager/SyncManager-flow.tsx index e91a5cf69..5e5aa946b 100644 --- a/src/transaction-flow/input/SyncManager/SyncManager-flow.tsx +++ b/src/transaction/user/input/SyncManager/SyncManager-flow.tsx @@ -7,13 +7,13 @@ import { useAbilities } from '@app/hooks/abilities/useAbilities' import { useAccountSafely } from '@app/hooks/account/useAccountSafely' import { useDnsImportData } from '@app/hooks/ensjs/dns/useDnsImportData' import { useNameType } from '@app/hooks/nameType/useNameType' +import { usePrimaryNameOrAddress } from '@app/hooks/reverseRecord/usePrimaryNameOrAddress' import { useNameDetails } from '@app/hooks/useNameDetails' -import { createTransactionItem, TransactionItem } from '@app/transaction-flow/transaction' -import { makeTransferNameOrSubnameTransactionItem } from '@app/transaction-flow/transaction/utils/makeTransferNameOrSubnameTransactionItem' -import TransactionLoader from '@app/transaction-flow/TransactionLoader' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' +import TransactionLoader from '@app/transaction/components/TransactionLoader' -import { usePrimaryNameOrAddress } from '../../../hooks/reverseRecord/usePrimaryNameOrAddress' +import { createUserTransaction, type GenericUserTransaction } from '../../transaction' +import { makeTransferNameOrSubnameTransactionItem } from '../../transaction/utils/makeTransferNameOrSubnameTransactionItem' import { checkCanSyncManager } from './utils/checkCanSyncManager' import { ErrorView } from './views/ErrorView' import { MainView } from './views/MainView' @@ -26,7 +26,7 @@ export type Props = { data: Data } & TransactionDialogPassthrough -const SyncManager = ({ data: { name }, dispatch, onDismiss }: Props) => { +const SyncManager = ({ data: { name }, onDismiss, setStage, setTransactions }: Props) => { const { t } = useTranslation('transactionFlow') const account = useAccountSafely() @@ -71,7 +71,7 @@ const SyncManager = ({ data: { name }, dispatch, onDismiss }: Props) => { const onClickNext = () => { const transactions = [ canSyncDNS - ? createTransactionItem('syncManager', { + ? createUserTransaction('syncManager', { name, address: account.address!, dnsImportData: dnsImportData.data!, @@ -90,18 +90,15 @@ const SyncManager = ({ data: { name }, dispatch, onDismiss }: Props) => { ( transaction, ): transaction is - | TransactionItem<'syncManager'> - | TransactionItem<'transferName'> - | TransactionItem<'transferSubname'> => !!transaction, + | GenericUserTransaction<'syncManager'> + | GenericUserTransaction<'transferName'> + | GenericUserTransaction<'transferSubname'> => !!transaction, ) if (transactions.length !== 1) return - dispatch({ - name: 'setTransactions', - payload: transactions, - }) - dispatch({ name: 'setFlowStage', payload: 'transaction' }) + setTransactions(transactions) + setStage('transaction') } return ( diff --git a/src/transaction-flow/input/SyncManager/utils/checkCanSyncManager.ts b/src/transaction/user/input/SyncManager/utils/checkCanSyncManager.ts similarity index 100% rename from src/transaction-flow/input/SyncManager/utils/checkCanSyncManager.ts rename to src/transaction/user/input/SyncManager/utils/checkCanSyncManager.ts diff --git a/src/transaction-flow/input/SyncManager/views/ErrorView.tsx b/src/transaction/user/input/SyncManager/views/ErrorView.tsx similarity index 100% rename from src/transaction-flow/input/SyncManager/views/ErrorView.tsx rename to src/transaction/user/input/SyncManager/views/ErrorView.tsx diff --git a/src/transaction-flow/input/SyncManager/views/MainView.tsx b/src/transaction/user/input/SyncManager/views/MainView.tsx similarity index 100% rename from src/transaction-flow/input/SyncManager/views/MainView.tsx rename to src/transaction/user/input/SyncManager/views/MainView.tsx diff --git a/src/transaction-flow/input/UnknownLabels/UnknownLabels-flow.tsx b/src/transaction/user/input/UnknownLabels/UnknownLabels-flow.tsx similarity index 60% rename from src/transaction-flow/input/UnknownLabels/UnknownLabels-flow.tsx rename to src/transaction/user/input/UnknownLabels/UnknownLabels-flow.tsx index e6fa9d93a..2cd59dbfb 100644 --- a/src/transaction-flow/input/UnknownLabels/UnknownLabels-flow.tsx +++ b/src/transaction/user/input/UnknownLabels/UnknownLabels-flow.tsx @@ -5,26 +5,27 @@ import { useForm } from 'react-hook-form' import { saveName } from '@ensdomains/ensjs/utils' import { useQueryOptions } from '@app/hooks/useQueryOptions' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' +import type { FlowInitialiserData } from '@app/transaction/slices/createFlowSlice' +import { useTransactionManager } from '@app/transaction/transactionManager' -import { TransactionDialogPassthrough, TransactionFlowItem } from '../../types' +import type { TransactionIntro } from '../../intro' +import type { UserTransaction } from '../../transaction' import { FormData, nameToFormData, UnknownLabelsForm } from './views/UnknownLabelsForm' type Data = { name: string - key: string - transactionFlowItem: TransactionFlowItem + flow: Pick } export type Props = { data: Data } & TransactionDialogPassthrough -const UnknownLabels = ({ - data: { name, key, transactionFlowItem }, - dispatch, - onDismiss, -}: Props) => { +const UnknownLabels = ({ data: { name, flow }, onDismiss }: Props) => { const queryClient = useQueryClient() + const getTransactions = useTransactionManager((s) => s.getFlowTransactions) + const startFlow = useTransactionManager((s) => s.startFlow) const formRef = useRef(null) @@ -51,34 +52,37 @@ const UnknownLabels = ({ saveName(newName) - const { transactions, intro } = transactionFlowItem + const { flowId, intro } = flow + const transactions = getTransactions(flow.flowId) - const newKey = key.replace(name, newName) + const newFlowId = flowId.replace(name, newName) const newTransactions = transactions.map((tx) => typeof tx.data === 'object' && 'name' in tx.data && tx.data.name ? { ...tx, data: { ...tx.data, name: newName } } : tx, - ) - - const newIntro = - intro && typeof intro.content.data === 'object' && intro.content.data.name + ) as UserTransaction[] + + const newIntro = ( + intro && + typeof intro.content.data === 'object' && + intro.content.data && + 'name' in intro.content.data && + intro.content.data?.name ? { ...intro, content: { ...intro.content, data: { ...intro.content.data, name: newName } }, } : intro + ) as TransactionIntro queryClient.resetQueries({ queryKey: validateKey, exact: true }) - dispatch({ - name: 'startFlow', - key: newKey, - payload: { - ...transactionFlowItem, - transactions: newTransactions, - intro: newIntro as any, - }, + startFlow({ + ...flow, + flowId: newFlowId, + transactions: newTransactions, + intro: newIntro, }) } diff --git a/src/transaction-flow/input/UnknownLabels/UnknownLabels.test.tsx b/src/transaction/user/input/UnknownLabels/UnknownLabels.test.tsx similarity index 100% rename from src/transaction-flow/input/UnknownLabels/UnknownLabels.test.tsx rename to src/transaction/user/input/UnknownLabels/UnknownLabels.test.tsx diff --git a/src/transaction-flow/input/UnknownLabels/views/UnknownLabelsForm.tsx b/src/transaction/user/input/UnknownLabels/views/UnknownLabelsForm.tsx similarity index 100% rename from src/transaction-flow/input/UnknownLabels/views/UnknownLabelsForm.tsx rename to src/transaction/user/input/UnknownLabels/views/UnknownLabelsForm.tsx diff --git a/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx b/src/transaction/user/input/VerifyProfile/VerifyProfile-flow.tsx similarity index 89% rename from src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx rename to src/transaction/user/input/VerifyProfile/VerifyProfile-flow.tsx index a3fd6eedc..3d6a5da2c 100644 --- a/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx +++ b/src/transaction/user/input/VerifyProfile/VerifyProfile-flow.tsx @@ -5,7 +5,7 @@ import { VERIFICATION_RECORD_KEY } from '@app/constants/verification' import { useOwner } from '@app/hooks/ensjs/public/useOwner' import { useProfile } from '@app/hooks/useProfile' import { useVerifiedRecords } from '@app/hooks/verification/useVerifiedRecords/useVerifiedRecords' -import { TransactionDialogPassthrough } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' import { SearchViewLoadingView } from '../SendName/views/SearchView/views/SearchViewLoadingView' import { DentityView } from './views/DentityView' @@ -23,7 +23,7 @@ export type Props = { data: Data } & TransactionDialogPassthrough -const VerifyProfile = ({ data: { name }, dispatch, onDismiss }: Props) => { +const VerifyProfile = ({ data: { name }, onDismiss, setStage, setTransactions }: Props) => { const [protocol, setProtocol] = useState(null) const { data: profile, isLoading: isProfileLoading } = useProfile({ name }) @@ -59,7 +59,8 @@ const VerifyProfile = ({ data: { name }, dispatch, onDismiss }: Props) => { address={_address!} resolverAddress={_resolverAddress!} verified={!!verificationData?.some(({ issuer }) => issuer === 'dentity')} - dispatch={dispatch} + setStage={setStage} + setTransactions={setTransactions} onBack={() => setProtocol(null)} /> ), diff --git a/src/transaction-flow/input/VerifyProfile/components/VerificationOptionButton.tsx b/src/transaction/user/input/VerifyProfile/components/VerificationOptionButton.tsx similarity index 100% rename from src/transaction-flow/input/VerifyProfile/components/VerificationOptionButton.tsx rename to src/transaction/user/input/VerifyProfile/components/VerificationOptionButton.tsx diff --git a/src/transaction-flow/input/VerifyProfile/utils/createDentityUrl.ts b/src/transaction/user/input/VerifyProfile/utils/createDentityUrl.ts similarity index 100% rename from src/transaction-flow/input/VerifyProfile/utils/createDentityUrl.ts rename to src/transaction/user/input/VerifyProfile/utils/createDentityUrl.ts diff --git a/src/transaction-flow/input/VerifyProfile/views/DentityView.tsx b/src/transaction/user/input/VerifyProfile/views/DentityView.tsx similarity index 83% rename from src/transaction-flow/input/VerifyProfile/views/DentityView.tsx rename to src/transaction/user/input/VerifyProfile/views/DentityView.tsx index 768702948..de9c78e68 100644 --- a/src/transaction-flow/input/VerifyProfile/views/DentityView.tsx +++ b/src/transaction/user/input/VerifyProfile/views/DentityView.tsx @@ -1,4 +1,3 @@ -import { Dispatch } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { Hash } from 'viem' @@ -6,8 +5,7 @@ import { Hash } from 'viem' import { Button, Dialog, Helper, Typography } from '@ensdomains/thorin' import TrashSVG from '@app/assets/Trash.svg' -import { createTransactionItem } from '@app/transaction-flow/transaction' -import { TransactionFlowAction } from '@app/transaction-flow/types' +import type { TransactionDialogPassthrough } from '@app/transaction/components/TransactionDialogManager' import { CenteredTypography } from '../../ProfileEditor/components/CenteredTypography' import { createDentityAuthUrl } from '../utils/createDentityUrl' @@ -53,41 +51,35 @@ export const DentityView = ({ verified, resolverAddress, onBack, - dispatch, + setStage, + setTransactions, }: { name: string address: Hash verified: boolean resolverAddress: Hash onBack?: () => void - dispatch: Dispatch -}) => { +} & Omit) => { const { t } = useTranslation('transactionFlow') // Clear transactions before going back const onBackAndCleanup = () => { - dispatch({ - name: 'setTransactions', - payload: [], - }) + setTransactions([]) onBack?.() } const onRemoveVerification = () => { - dispatch({ - name: 'setTransactions', - payload: [ - createTransactionItem('removeVerificationRecord', { + setTransactions([ + { + name: 'removeVerificationRecord', + data: { name, verifier: 'dentity', resolverAddress, - }), - ], - }) - dispatch({ - name: 'setFlowStage', - payload: 'transaction', - }) + }, + }, + ]) + setStage('transaction') } return ( diff --git a/src/transaction-flow/input/VerifyProfile/views/VerificationOptionsList.tsx b/src/transaction/user/input/VerifyProfile/views/VerificationOptionsList.tsx similarity index 100% rename from src/transaction-flow/input/VerifyProfile/views/VerificationOptionsList.tsx rename to src/transaction/user/input/VerifyProfile/views/VerificationOptionsList.tsx diff --git a/src/transaction/user/intro.tsx b/src/transaction/user/intro.tsx new file mode 100644 index 000000000..76ba3e69e --- /dev/null +++ b/src/transaction/user/intro.tsx @@ -0,0 +1,64 @@ +import type { TOptions } from 'i18next' +import { ComponentProps } from 'react' + +import { ChangePrimaryName } from './intro/ChangePrimaryName' +import { GenericWithDescription } from './intro/GenericWithDescription' +import { MigrateAndUpdateResolver } from './intro/MigrateAndUpdateResolver' +import { SyncManager } from './intro/SyncManager' +import { WrapName } from './intro/WrapName' + +export const transactionIntroComponents = { + WrapName, + MigrateAndUpdateResolver, + SyncManager, + ChangePrimaryName, + GenericWithDescription, +} + +export type TransactionIntroComponent = typeof transactionIntroComponents +export type TransactionIntroComponentName = keyof TransactionIntroComponent + +export type TransactionIntroComponentParameters = + unknown extends ComponentProps + ? undefined + : ComponentProps + +export const createTransactionIntro = ( + name: name, + data: ComponentProps, +) => ({ + name, + data, +}) + +export const AnyTransactionIntro = ({ + name, + data, +}: GenericTransactionIntro) => { + const Content = transactionIntroComponents[name] + return +} + +type GenericTransactionIntro< + name extends TransactionIntroComponentName = TransactionIntroComponentName, + data extends + TransactionIntroComponentParameters = TransactionIntroComponentParameters, +> = { + name: name + data: data +} + +export type TransactionIntroContent = { + [name in TransactionIntroComponentName]: GenericTransactionIntro +}[TransactionIntroComponentName] + +type StoredTranslationReference = [key: string, options?: TOptions] + +export type TransactionIntro< + name extends TransactionIntroComponentName = TransactionIntroComponentName, +> = { + title: StoredTranslationReference + leadingLabel?: StoredTranslationReference + trailingLabel?: StoredTranslationReference + content: GenericTransactionIntro +} diff --git a/src/transaction-flow/intro/ChangePrimaryName.tsx b/src/transaction/user/intro/ChangePrimaryName.tsx similarity index 90% rename from src/transaction-flow/intro/ChangePrimaryName.tsx rename to src/transaction/user/intro/ChangePrimaryName.tsx index 212ab1934..8c3d503a5 100644 --- a/src/transaction-flow/intro/ChangePrimaryName.tsx +++ b/src/transaction/user/intro/ChangePrimaryName.tsx @@ -17,7 +17,8 @@ const DescriptionWrapper = styled(Typography)( `, ) -export const ChangePrimaryName = () => { +// eslint-disable-next-line no-empty-pattern +export const ChangePrimaryName = ({}: {}) => { const { t } = useTranslation('profile') return ( diff --git a/src/transaction-flow/intro/GenericWithDescription.tsx b/src/transaction/user/intro/GenericWithDescription.tsx similarity index 100% rename from src/transaction-flow/intro/GenericWithDescription.tsx rename to src/transaction/user/intro/GenericWithDescription.tsx diff --git a/src/transaction-flow/intro/MigrateAndUpdateResolver.tsx b/src/transaction/user/intro/MigrateAndUpdateResolver.tsx similarity index 91% rename from src/transaction-flow/intro/MigrateAndUpdateResolver.tsx rename to src/transaction/user/intro/MigrateAndUpdateResolver.tsx index 6fd3ecc67..2833696c9 100644 --- a/src/transaction-flow/intro/MigrateAndUpdateResolver.tsx +++ b/src/transaction/user/intro/MigrateAndUpdateResolver.tsx @@ -19,7 +19,8 @@ const DescriptionWrapper = styled(Typography)( `, ) -export const MigrateAndUpdateResolver = () => { +// eslint-disable-next-line no-empty-pattern +export const MigrateAndUpdateResolver = ({}: {}) => { const { t } = useTranslation('transactionFlow') return ( <> diff --git a/src/transaction-flow/intro/SyncManager.tsx b/src/transaction/user/intro/SyncManager.tsx similarity index 100% rename from src/transaction-flow/intro/SyncManager.tsx rename to src/transaction/user/intro/SyncManager.tsx diff --git a/src/transaction-flow/intro/WrapName.tsx b/src/transaction/user/intro/WrapName.tsx similarity index 100% rename from src/transaction-flow/intro/WrapName.tsx rename to src/transaction/user/intro/WrapName.tsx diff --git a/src/transaction/user/transaction.ts b/src/transaction/user/transaction.ts new file mode 100644 index 000000000..bcf83471b --- /dev/null +++ b/src/transaction/user/transaction.ts @@ -0,0 +1,109 @@ +import type { TargetChain } from '@app/constants/chains' + +// eslint-disable-next-line @typescript-eslint/naming-convention +import __dev_failure from './transaction/__dev_failure' +import approveDnsRegistrar from './transaction/approveDnsRegistrar' +import approveNameWrapper from './transaction/approveNameWrapper' +import burnFuses from './transaction/burnFuses' +import changePermissions from './transaction/changePermissions' +import claimDnsName from './transaction/claimDnsName' +import commitName from './transaction/commitName' +import createSubname from './transaction/createSubname' +import deleteSubname from './transaction/deleteSubname' +import extendNames from './transaction/extendNames' +import importDnsName from './transaction/importDnsName' +import migrateProfile from './transaction/migrateProfile' +import migrateProfileWithReset from './transaction/migrateProfileWithReset' +import registerName from './transaction/registerName' +import removeVerificationRecord from './transaction/removeVerificationRecord' +import resetPrimaryName from './transaction/resetPrimaryName' +import resetProfile from './transaction/resetProfile' +import resetProfileWithRecords from './transaction/resetProfileWithRecords' +import setPrimaryName from './transaction/setPrimaryName' +import syncManager from './transaction/syncManager' +import testSendName from './transaction/testSendName' +import transferController from './transaction/transferController' +import transferName from './transaction/transferName' +import transferSubname from './transaction/transferSubname' +import unwrapName from './transaction/unwrapName' +import updateEthAddress from './transaction/updateEthAddress' +import updateProfile from './transaction/updateProfile' +import updateProfileRecords from './transaction/updateProfileRecords' +import updateResolver from './transaction/updateResolver' +import updateVerificationRecord from './transaction/updateVerificationRecord' +import wrapName from './transaction/wrapName' + +export const userTransactions = { + approveDnsRegistrar, + approveNameWrapper, + burnFuses, + changePermissions, + claimDnsName, + commitName, + createSubname, + deleteSubname, + extendNames, + importDnsName, + migrateProfile, + migrateProfileWithReset, + registerName, + resetPrimaryName, + resetProfile, + resetProfileWithRecords, + setPrimaryName, + syncManager, + testSendName, + transferController, + transferName, + transferSubname, + unwrapName, + updateEthAddress, + updateProfile, + updateProfileRecords, + updateResolver, + wrapName, + updateVerificationRecord, + removeVerificationRecord, + // eslint-disable-next-line @typescript-eslint/naming-convention + __dev_failure, +} + +export type UserTransactionObject = typeof userTransactions +export type UserTransactionName = keyof UserTransactionObject + +export type UserTransactionParameters = Parameters< + UserTransactionObject[name]['transaction'] +>[0] + +export type UserTransactionData = + UserTransactionParameters['data'] + +export type UserTransactionReturnType = ReturnType< + UserTransactionObject[name]['transaction'] +> + +export const createUserTransaction = ( + name: name, + data: UserTransactionData, +) => ({ + name, + data, +}) + +export const createTransactionRequest = ({ + name, + ...rest +}: { name: name } & UserTransactionParameters): UserTransactionReturnType => { + // i think this has to be any :( + return userTransactions[name].transaction({ ...rest } as any) as UserTransactionReturnType +} + +export type GenericUserTransaction = { + name: name + data: UserTransactionData + targetChainId?: TargetChain['id'] +} + +export type UserTransaction = { + [name in UserTransactionName]: GenericUserTransaction +}[UserTransactionName] diff --git a/src/transaction/user/transaction/__dev_failure.ts b/src/transaction/user/transaction/__dev_failure.ts new file mode 100644 index 000000000..70c386cfc --- /dev/null +++ b/src/transaction/user/transaction/__dev_failure.ts @@ -0,0 +1,25 @@ +import { Transaction, TransactionDisplayItem, type TransactionFunctionParameters } from '@app/types' + +type Data = {} + +// eslint-disable-next-line no-empty-pattern +const displayItems = ({}: Data): TransactionDisplayItem[] => [ + { + label: 'action', + value: '__dev_failure', + }, + { + label: 'info', + value: 'DO NOT USE', + }, +] + +// eslint-disable-next-line no-empty-pattern +const transaction = async ({}: TransactionFunctionParameters) => { + return { + to: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', + data: '0x1231237123423423', + } as const +} + +export default { displayItems, transaction } satisfies Transaction diff --git a/src/transaction-flow/transaction/approveDnsRegistrar.ts b/src/transaction/user/transaction/approveDnsRegistrar.ts similarity index 100% rename from src/transaction-flow/transaction/approveDnsRegistrar.ts rename to src/transaction/user/transaction/approveDnsRegistrar.ts diff --git a/src/transaction-flow/transaction/approveNameWrapper.ts b/src/transaction/user/transaction/approveNameWrapper.ts similarity index 100% rename from src/transaction-flow/transaction/approveNameWrapper.ts rename to src/transaction/user/transaction/approveNameWrapper.ts diff --git a/src/transaction-flow/transaction/burnFuses.ts b/src/transaction/user/transaction/burnFuses.ts similarity index 100% rename from src/transaction-flow/transaction/burnFuses.ts rename to src/transaction/user/transaction/burnFuses.ts diff --git a/src/transaction-flow/transaction/changePermissions.ts b/src/transaction/user/transaction/changePermissions.ts similarity index 100% rename from src/transaction-flow/transaction/changePermissions.ts rename to src/transaction/user/transaction/changePermissions.ts diff --git a/src/transaction-flow/transaction/claimDnsName.ts b/src/transaction/user/transaction/claimDnsName.ts similarity index 100% rename from src/transaction-flow/transaction/claimDnsName.ts rename to src/transaction/user/transaction/claimDnsName.ts diff --git a/src/transaction-flow/transaction/commitName.ts b/src/transaction/user/transaction/commitName.ts similarity index 100% rename from src/transaction-flow/transaction/commitName.ts rename to src/transaction/user/transaction/commitName.ts diff --git a/src/transaction-flow/transaction/createSubname.ts b/src/transaction/user/transaction/createSubname.ts similarity index 100% rename from src/transaction-flow/transaction/createSubname.ts rename to src/transaction/user/transaction/createSubname.ts diff --git a/src/transaction-flow/transaction/deleteSubname.ts b/src/transaction/user/transaction/deleteSubname.ts similarity index 100% rename from src/transaction-flow/transaction/deleteSubname.ts rename to src/transaction/user/transaction/deleteSubname.ts diff --git a/src/transaction-flow/transaction/extendNames.ts b/src/transaction/user/transaction/extendNames.ts similarity index 98% rename from src/transaction-flow/transaction/extendNames.ts rename to src/transaction/user/transaction/extendNames.ts index ac2ff598a..9a3cd1520 100644 --- a/src/transaction-flow/transaction/extendNames.ts +++ b/src/transaction/user/transaction/extendNames.ts @@ -5,7 +5,7 @@ import { renewNames } from '@ensdomains/ensjs/wallet' import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types' -import { calculateValueWithBuffer, formatDurationOfDates } from '../../utils/utils' +import { calculateValueWithBuffer, formatDurationOfDates } from '../../../utils/utils' type Data = { names: string[] diff --git a/src/transaction-flow/transaction/importDnsName.ts b/src/transaction/user/transaction/importDnsName.ts similarity index 100% rename from src/transaction-flow/transaction/importDnsName.ts rename to src/transaction/user/transaction/importDnsName.ts diff --git a/src/transaction-flow/transaction/migrateProfile.ts b/src/transaction/user/transaction/migrateProfile.ts similarity index 100% rename from src/transaction-flow/transaction/migrateProfile.ts rename to src/transaction/user/transaction/migrateProfile.ts diff --git a/src/transaction-flow/transaction/migrateProfileWithReset.ts b/src/transaction/user/transaction/migrateProfileWithReset.ts similarity index 100% rename from src/transaction-flow/transaction/migrateProfileWithReset.ts rename to src/transaction/user/transaction/migrateProfileWithReset.ts diff --git a/src/transaction-flow/transaction/registerName.test.ts b/src/transaction/user/transaction/registerName.test.ts similarity index 100% rename from src/transaction-flow/transaction/registerName.test.ts rename to src/transaction/user/transaction/registerName.test.ts diff --git a/src/transaction-flow/transaction/registerName.ts b/src/transaction/user/transaction/registerName.ts similarity index 100% rename from src/transaction-flow/transaction/registerName.ts rename to src/transaction/user/transaction/registerName.ts diff --git a/src/transaction-flow/transaction/removeVerificationRecord.ts b/src/transaction/user/transaction/removeVerificationRecord.ts similarity index 100% rename from src/transaction-flow/transaction/removeVerificationRecord.ts rename to src/transaction/user/transaction/removeVerificationRecord.ts diff --git a/src/transaction-flow/transaction/resetPrimaryName.ts b/src/transaction/user/transaction/resetPrimaryName.ts similarity index 100% rename from src/transaction-flow/transaction/resetPrimaryName.ts rename to src/transaction/user/transaction/resetPrimaryName.ts diff --git a/src/transaction-flow/transaction/resetProfile.ts b/src/transaction/user/transaction/resetProfile.ts similarity index 100% rename from src/transaction-flow/transaction/resetProfile.ts rename to src/transaction/user/transaction/resetProfile.ts diff --git a/src/transaction-flow/transaction/resetProfileWithRecords.ts b/src/transaction/user/transaction/resetProfileWithRecords.ts similarity index 100% rename from src/transaction-flow/transaction/resetProfileWithRecords.ts rename to src/transaction/user/transaction/resetProfileWithRecords.ts diff --git a/src/transaction-flow/transaction/setPrimaryName.ts b/src/transaction/user/transaction/setPrimaryName.ts similarity index 100% rename from src/transaction-flow/transaction/setPrimaryName.ts rename to src/transaction/user/transaction/setPrimaryName.ts diff --git a/src/transaction-flow/transaction/syncManager.ts b/src/transaction/user/transaction/syncManager.ts similarity index 100% rename from src/transaction-flow/transaction/syncManager.ts rename to src/transaction/user/transaction/syncManager.ts diff --git a/src/transaction-flow/transaction/testSendName.ts b/src/transaction/user/transaction/testSendName.ts similarity index 100% rename from src/transaction-flow/transaction/testSendName.ts rename to src/transaction/user/transaction/testSendName.ts diff --git a/src/transaction-flow/transaction/transferController.ts b/src/transaction/user/transaction/transferController.ts similarity index 100% rename from src/transaction-flow/transaction/transferController.ts rename to src/transaction/user/transaction/transferController.ts diff --git a/src/transaction-flow/transaction/transferName.ts b/src/transaction/user/transaction/transferName.ts similarity index 100% rename from src/transaction-flow/transaction/transferName.ts rename to src/transaction/user/transaction/transferName.ts diff --git a/src/transaction-flow/transaction/transferSubname.ts b/src/transaction/user/transaction/transferSubname.ts similarity index 100% rename from src/transaction-flow/transaction/transferSubname.ts rename to src/transaction/user/transaction/transferSubname.ts diff --git a/src/transaction-flow/transaction/unwrapName.test.ts b/src/transaction/user/transaction/unwrapName.test.ts similarity index 100% rename from src/transaction-flow/transaction/unwrapName.test.ts rename to src/transaction/user/transaction/unwrapName.test.ts diff --git a/src/transaction-flow/transaction/unwrapName.ts b/src/transaction/user/transaction/unwrapName.ts similarity index 100% rename from src/transaction-flow/transaction/unwrapName.ts rename to src/transaction/user/transaction/unwrapName.ts diff --git a/src/transaction-flow/transaction/updateEthAddress.ts b/src/transaction/user/transaction/updateEthAddress.ts similarity index 100% rename from src/transaction-flow/transaction/updateEthAddress.ts rename to src/transaction/user/transaction/updateEthAddress.ts diff --git a/src/transaction-flow/transaction/updateProfile.ts b/src/transaction/user/transaction/updateProfile.ts similarity index 96% rename from src/transaction-flow/transaction/updateProfile.ts rename to src/transaction/user/transaction/updateProfile.ts index 0401017db..f9e8bbdfb 100644 --- a/src/transaction-flow/transaction/updateProfile.ts +++ b/src/transaction/user/transaction/updateProfile.ts @@ -5,8 +5,7 @@ import type { RecordOptions } from '@ensdomains/ensjs/utils' import { setRecords } from '@ensdomains/ensjs/wallet' import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types' - -import { recordOptionsToToupleList } from '../../utils/records' +import { recordOptionsToToupleList } from '@app/utils/records' type Data = { name: string diff --git a/src/transaction-flow/transaction/updateProfileRecords.ts b/src/transaction/user/transaction/updateProfileRecords.ts similarity index 96% rename from src/transaction-flow/transaction/updateProfileRecords.ts rename to src/transaction/user/transaction/updateProfileRecords.ts index 93d55e9ca..b378cca7b 100644 --- a/src/transaction-flow/transaction/updateProfileRecords.ts +++ b/src/transaction/user/transaction/updateProfileRecords.ts @@ -7,7 +7,7 @@ import { getProfileRecordsDiff, profileRecordsToRecordOptions, profileRecordsToRecordOptionsWithDeleteAbiArray, -} from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +} from '@app/components/pages/register/steps/Profile/profileRecordUtils' import { ProfileRecord } from '@app/constants/profileRecordOptions' import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types' import { recordOptionsToToupleList } from '@app/utils/records' diff --git a/src/transaction-flow/transaction/updateResolver.ts b/src/transaction/user/transaction/updateResolver.ts similarity index 95% rename from src/transaction-flow/transaction/updateResolver.ts rename to src/transaction/user/transaction/updateResolver.ts index e43fa39cd..28f350a68 100644 --- a/src/transaction-flow/transaction/updateResolver.ts +++ b/src/transaction/user/transaction/updateResolver.ts @@ -5,7 +5,7 @@ import { setResolver } from '@ensdomains/ensjs/wallet' import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types' -import { shortenAddress } from '../../utils/utils' +import { shortenAddress } from '../../../utils/utils' type Data = { name: string diff --git a/src/transaction-flow/transaction/updateVerificationRecord.ts b/src/transaction/user/transaction/updateVerificationRecord.ts similarity index 100% rename from src/transaction-flow/transaction/updateVerificationRecord.ts rename to src/transaction/user/transaction/updateVerificationRecord.ts diff --git a/src/transaction-flow/transaction/utils/makeTransferNameOrSubnameTransactionItem.ts b/src/transaction/user/transaction/utils/makeTransferNameOrSubnameTransactionItem.ts similarity index 81% rename from src/transaction-flow/transaction/utils/makeTransferNameOrSubnameTransactionItem.ts rename to src/transaction/user/transaction/utils/makeTransferNameOrSubnameTransactionItem.ts index 99f7086ae..341c6cfa6 100644 --- a/src/transaction-flow/transaction/utils/makeTransferNameOrSubnameTransactionItem.ts +++ b/src/transaction/user/transaction/utils/makeTransferNameOrSubnameTransactionItem.ts @@ -3,7 +3,7 @@ import { Address } from 'viem' import type { useAbilities } from '@app/hooks/abilities/useAbilities' -import { createTransactionItem, TransactionItem } from '..' +import { createUserTransaction, UserTransaction } from '../../transaction' type MakeTransferNameOrSubnameTransactionItemParams = { name: string @@ -19,7 +19,7 @@ export const makeTransferNameOrSubnameTransactionItem = ({ sendType, isOwnerOrManager, abilities, -}: MakeTransferNameOrSubnameTransactionItemParams): TransactionItem | null => { +}: MakeTransferNameOrSubnameTransactionItemParams): UserTransaction | null => { return ( match([ isOwnerOrManager, @@ -27,7 +27,7 @@ export const makeTransferNameOrSubnameTransactionItem = ({ abilities?.sendNameFunctionCallDetails?.[sendType]?.contract, ]) .with([true, 'sendOwner', P.not(P.nullish)], ([, , contract]) => - createTransactionItem('transferName', { + createUserTransaction('transferName', { name, newOwnerAddress, sendType: 'sendOwner', @@ -35,7 +35,7 @@ export const makeTransferNameOrSubnameTransactionItem = ({ }), ) .with([true, 'sendManager', 'registrar'], () => - createTransactionItem('transferName', { + createUserTransaction('transferName', { name, newOwnerAddress, sendType: 'sendManager', @@ -44,7 +44,7 @@ export const makeTransferNameOrSubnameTransactionItem = ({ }), ) .with([true, 'sendManager', P.union('registry', 'nameWrapper')], ([, , contract]) => - createTransactionItem('transferName', { + createUserTransaction('transferName', { name, newOwnerAddress, sendType: 'sendManager', @@ -53,7 +53,7 @@ export const makeTransferNameOrSubnameTransactionItem = ({ ) // A parent name can only transfer the manager .with([false, 'sendManager', P.union('registry', 'nameWrapper')], ([, , contract]) => - createTransactionItem('transferSubname', { + createUserTransaction('transferSubname', { name, newOwnerAddress, contract, diff --git a/src/transaction-flow/transaction/wrapName.ts b/src/transaction/user/transaction/wrapName.ts similarity index 100% rename from src/transaction-flow/transaction/wrapName.ts rename to src/transaction/user/transaction/wrapName.ts diff --git a/src/types/index.ts b/src/types/index.ts index 7c15146bd..6d371d5ed 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -100,7 +100,6 @@ export interface Transaction { transaction: ( params: TransactionFunctionParameters, ) => Promise | BasicTransactionRequest - helper?: (data: TData, t: TFunction<'translation', undefined>) => undefined | HelperProps backToInput?: boolean } diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 279bf7cde..f9516d00f 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -1,3 +1,7 @@ +import { mainnet } from 'viem/chains' + +import type { SupportedChain } from '@app/constants/chains' + declare global { interface Window { plausible: any @@ -10,10 +14,6 @@ function isProduction() { } } -function isMainnet(chain: string) { - return chain === 'mainnet' -} - export function setUtm() { if (typeof window !== 'undefined') { const urlParams = new URLSearchParams(window.location.search) @@ -32,7 +32,7 @@ export const setupAnalytics = () => { setUtm() } -export const trackEvent = async (type: string, chain: string) => { +export const trackEvent = async (type: string, chainId: SupportedChain['id']) => { const referrer = getUtm() function track() { if (typeof window !== 'undefined' && window.plausible) { @@ -43,8 +43,8 @@ export const trackEvent = async (type: string, chain: string) => { }) } } - console.log('Event triggering', type, chain) - if (isProduction() && isMainnet(chain)) { + console.log('Event triggering', type, chainId) + if (isProduction() && chainId === mainnet.id) { track() } else { console.log( diff --git a/src/utils/chains/makeLocalhostChainWithEns.ts b/src/utils/chains/makeLocalhostChainWithEns.ts index 295e9c1e8..43bc1d837 100644 --- a/src/utils/chains/makeLocalhostChainWithEns.ts +++ b/src/utils/chains/makeLocalhostChainWithEns.ts @@ -8,6 +8,12 @@ export const makeLocalhostChainWithEns = ( ) => { return { ...localhost, + blockExplorers: { + default: { + name: 'Etherscan', + url: 'https://dummy.etherscan.io', + }, + }, contracts: { ...localhost.contracts, ensRegistry: { diff --git a/src/utils/getChainName.ts b/src/utils/getChainName.ts index 960c9cda9..de5219648 100644 --- a/src/utils/getChainName.ts +++ b/src/utils/getChainName.ts @@ -1,7 +1,9 @@ -import { Config } from 'wagmi' +import { getSupportedChainById, type SupportedChain } from '@app/constants/chains' -export const getChainName = (config: Config, { chainId }: { chainId: number }) => { +export type ChainName = Lowercase> | 'mainnet' + +export const getChainName = (chainId: SupportedChain['id'] | undefined): ChainName => { if (chainId === 1 || !chainId) return 'mainnet' - const chainName = config.getClient({ chainId }).chain.name - return chainName.toLowerCase() + const chain = getSupportedChainById(chainId)! + return chain.name.toLowerCase() as ChainName } diff --git a/src/utils/query/getSourceChainId.ts b/src/utils/query/getSourceChainId.ts new file mode 100644 index 000000000..04914bee6 --- /dev/null +++ b/src/utils/query/getSourceChainId.ts @@ -0,0 +1,6 @@ +import { getSupportedChainById, type SourceChain, type TargetChain } from '@app/constants/chains' + +export const getSourceChainId = (targetChainId: TargetChain['id']): SourceChain['id'] => { + const chain = getSupportedChainById(targetChainId)! + return (chain.sourceId ?? chain.id) as SourceChain['id'] +} diff --git a/src/utils/query/wagmi.ts b/src/utils/query/wagmi.ts index 4799fefbc..31506d256 100644 --- a/src/utils/query/wagmi.ts +++ b/src/utils/query/wagmi.ts @@ -1,11 +1,10 @@ import { createClient, type FallbackTransport, type HttpTransport, type Transport } from 'viem' import { createConfig, createStorage, fallback, http } from 'wagmi' -import { goerli, holesky, localhost, mainnet, sepolia } from 'wagmi/chains' +import { holesky, localhost, mainnet, sepolia } from 'wagmi/chains' import { ccipRequest } from '@ensdomains/ensjs/utils' import { - goerliWithEns, holeskyWithEns, localhostWithEns, mainnetWithEns, @@ -83,7 +82,6 @@ const localStorageWithInvertMiddleware = (): Storage | undefined => { const chains = [ ...(isLocalProvider ? ([localhostWithEns] as const) : ([] as const)), mainnetWithEns, - goerliWithEns, sepoliaWithEns, holeskyWithEns, ] as const @@ -99,8 +97,7 @@ const transports = { })), [mainnet.id]: initialiseTransports('mainnet', [infuraUrl, cloudflareUrl, tenderlyUrl]), [sepolia.id]: initialiseTransports('sepolia', [infuraUrl, cloudflareUrl, tenderlyUrl]), - [goerli.id]: initialiseTransports('goerli', [infuraUrl, cloudflareUrl, tenderlyUrl]), - [holesky.id]: initialiseTransports('holesky', [tenderlyUrl]), + [holesky.id]: initialiseTransports('holesky', [infuraUrl, tenderlyUrl]), } as const const wagmiConfig_ = createConfig({ diff --git a/src/utils/records/categoriseProfileTextRecords.ts b/src/utils/records/categoriseProfileTextRecords.ts index a1a7c3481..dee167d4a 100644 --- a/src/utils/records/categoriseProfileTextRecords.ts +++ b/src/utils/records/categoriseProfileTextRecords.ts @@ -9,7 +9,7 @@ import { supportedSocialRecordKeys, } from '@app/constants/supportedSocialRecordKeys' import { VERIFICATION_RECORD_KEY } from '@app/constants/verification' -import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' import { contentHashToString } from '../contenthash' import { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 7dccb6c6c..e6ae06b60 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -5,6 +5,11 @@ import { Eth2ldName } from '@ensdomains/ensjs/dist/types/types' import { GetPriceReturnType } from '@ensdomains/ensjs/public' import { DecodedFuses } from '@ensdomains/ensjs/utils' +import { + getSupportedChainById, + type GetSupportedChainById, + type SupportedChain, +} from '@app/constants/chains' import { KNOWN_RESOLVER_DATA } from '@app/constants/resolverAddressData' import { CURRENCY_FLUCTUATION_BUFFER_PERCENTAGE } from './constants' @@ -85,8 +90,23 @@ export const formatDurationOfDates = ({ return durationStrings.join(', ') + postFix } -export const makeEtherscanLink = (data: string, network?: string, route: string = 'tx') => - `https://${!network || network === 'mainnet' ? '' : `${network}.`}etherscan.io/${route}/${data}` +export const createEtherscanLink = < + const data extends string, + const chainId extends SupportedChain['id'], + const route extends string | undefined = 'tx', +>({ + data, + chainId, + route = 'tx', +}: { + data: data + chainId: chainId + route?: route +}) => { + const chain = getSupportedChainById(chainId) + const baseUrl = chain.blockExplorers.default.url + return `${baseUrl}/${route}/${data}` as `${GetSupportedChainById['blockExplorers']['default']['url']}/${route}/${data}` +} export const isBrowser = !!( typeof window !== 'undefined' && diff --git a/src/utils/verification/getVerifierData.ts b/src/utils/verification/getVerifierData.ts index caf237dae..99eed0cc5 100644 --- a/src/utils/verification/getVerifierData.ts +++ b/src/utils/verification/getVerifierData.ts @@ -1,5 +1,5 @@ -import { createDentityPublicProfileUrl } from '@app/transaction-flow/input/VerifyProfile/utils/createDentityUrl' -import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import { createDentityPublicProfileUrl } from '@app/transaction/user/input/VerifyProfile/utils/createDentityUrl' +import type { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' export const getVerifierData = (key: VerificationProtocol, value: string) => { switch (key) { diff --git a/src/utils/verification/isVerificationProtocol.ts b/src/utils/verification/isVerificationProtocol.ts index 4e4a77fdb..b6a52b371 100644 --- a/src/utils/verification/isVerificationProtocol.ts +++ b/src/utils/verification/isVerificationProtocol.ts @@ -1,5 +1,5 @@ import { VERIFICATION_PROTOCOLS } from '@app/constants/verification' -import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' export const isVerificationProtocol = (value: string): value is VerificationProtocol => { return VERIFICATION_PROTOCOLS.includes(value as VerificationProtocol) diff --git a/src/utils/verification/labelForVerificationProtocol.ts b/src/utils/verification/labelForVerificationProtocol.ts index b673b833e..8829645f3 100644 --- a/src/utils/verification/labelForVerificationProtocol.ts +++ b/src/utils/verification/labelForVerificationProtocol.ts @@ -1,4 +1,4 @@ -import type { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' +import type { VerificationProtocol } from '@app/transaction/user/input/VerifyProfile/VerifyProfile-flow' export const labelForVerificationProtocol = (protocol: VerificationProtocol) => { if (protocol === 'dentity') return 'dentity.com' diff --git a/tsconfig.json b/tsconfig.json index f00fa1841..481985697 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -70,6 +70,8 @@ "**/*.ignore.tsx", "functions/**/*", "deploy/**/*", + "**/*.test.ts", + "**/*.test.tsx", "hardhat.config.ts" ] -} \ No newline at end of file +}