From 1eabc7803b6f2e4cffde7fb260e5304d15ba72c2 Mon Sep 17 00:00:00 2001 From: 0xGabi Date: Thu, 2 Jun 2022 16:35:40 -0300 Subject: [PATCH 1/4] chore: draft --- .../screens/ScreenConnecting.tsx | 2 +- .../screens/ScreenPromptedAction.tsx | 2 +- .../FunctionDescriptor/index.tsx | 14 +- .../{AccountModule => }/LoadingRing.tsx | 0 app/components/ModalFlows/ModalFlowBase.tsx | 218 +++++++++++++ .../MultiModal/ConnectWallet/ConnectWallet.js | 77 +++++ .../ConnectWallet/ConnectWalletScreens.js | 61 ++++ .../ConnectWallet/ScreenErrorWrapper.js | 17 + .../ConnectWallet/ScreenProvidersWrapper.js | 35 ++ app/components/MultiModal/MultiModal.tsx | 53 ++++ .../MultiModal/MultiModalProvider.tsx | 81 +++++ .../MultiModal/MultiModalScreens.js | 300 ++++++++++++++++++ app/hooks/useSteps.ts | 88 +++++ package.json | 1 + yarn.lock | 254 ++++++++++++++- 15 files changed, 1199 insertions(+), 4 deletions(-) rename app/components/{AccountModule => }/LoadingRing.tsx (100%) create mode 100644 app/components/ModalFlows/ModalFlowBase.tsx create mode 100644 app/components/MultiModal/ConnectWallet/ConnectWallet.js create mode 100644 app/components/MultiModal/ConnectWallet/ConnectWalletScreens.js create mode 100644 app/components/MultiModal/ConnectWallet/ScreenErrorWrapper.js create mode 100644 app/components/MultiModal/ConnectWallet/ScreenProvidersWrapper.js create mode 100644 app/components/MultiModal/MultiModal.tsx create mode 100644 app/components/MultiModal/MultiModalProvider.tsx create mode 100644 app/components/MultiModal/MultiModalScreens.js create mode 100644 app/hooks/useSteps.ts diff --git a/app/components/AccountModule/screens/ScreenConnecting.tsx b/app/components/AccountModule/screens/ScreenConnecting.tsx index 20fbf10..c1dc2ba 100644 --- a/app/components/AccountModule/screens/ScreenConnecting.tsx +++ b/app/components/AccountModule/screens/ScreenConnecting.tsx @@ -2,7 +2,7 @@ import { GU, textStyle, useTheme } from "@blossom-labs/rosette-ui"; import styled from "styled-components"; import { getWalletIconPath } from "../helpers"; -import { LoadingRing } from "../LoadingRing"; +import { LoadingRing } from "../../LoadingRing"; import { useAccounModuleStore } from "../use-account-module-store"; export const ScreenConnecting = () => { diff --git a/app/components/AccountModule/screens/ScreenPromptedAction.tsx b/app/components/AccountModule/screens/ScreenPromptedAction.tsx index fbcc895..5380c12 100644 --- a/app/components/AccountModule/screens/ScreenPromptedAction.tsx +++ b/app/components/AccountModule/screens/ScreenPromptedAction.tsx @@ -1,7 +1,7 @@ import { GU, textStyle } from "@blossom-labs/rosette-ui/"; import styled from "styled-components"; -import { LoadingRing } from "../LoadingRing"; +import { LoadingRing } from "../../LoadingRing"; import { useAccounModuleStore } from "../use-account-module-store"; export const ScreenPromptedAction = () => { diff --git a/app/components/ContractDescriptorScreen/FunctionDescriptor/index.tsx b/app/components/ContractDescriptorScreen/FunctionDescriptor/index.tsx index 46135da..faa78c3 100644 --- a/app/components/ContractDescriptorScreen/FunctionDescriptor/index.tsx +++ b/app/components/ContractDescriptorScreen/FunctionDescriptor/index.tsx @@ -7,6 +7,7 @@ import { TextInput, Header, textStyle, + Info, } from "@blossom-labs/rosette-ui"; import { forwardRef, memo, useCallback, useState } from "react"; import type { @@ -21,6 +22,8 @@ import { actions } from "../use-contract-descriptor-store"; import type { Function } from "../use-contract-descriptor-store"; import { DescriptionField } from "./DescriptionField"; import { canTab, getSelectionRange } from "~/utils/client/selection.client"; +import { useTxDescribe } from "@blossom-labs/rosette-react"; +import { constants } from "ethers"; type FunctionDescriptorProps = { fnDescriptorEntry: Function; @@ -33,6 +36,7 @@ export const FunctionDescriptor = memo( ({ fnDescriptorEntry, description, onEntryChange, ...props }, ref) => { const [showModal, setShowModal] = useState(false); const [callData, setCallData] = useState(""); + const [testDescriptionError, setTestDescriptionError] = useState(""); const entry = fnDescriptorEntry.entry; const { notice, status } = entry || {}; const descriptorStatus = status || FnDescriptionStatus.Available; @@ -69,7 +73,14 @@ export const FunctionDescriptor = memo( setShowModal(true); }; - const handleDescribeCalldata = () => {}; + const handleDescribeCalldata = useCallback( () => { + useTxDescribe({ + to: constants.AddressZero, + data: callData + }) + setTestDescriptionError() + },[]); + return ( @@ -109,6 +120,7 @@ export const FunctionDescriptor = memo( wide /> + + + + + ) +} + +export default ConectWallet diff --git a/app/components/MultiModal/ConnectWallet/ConnectWalletScreens.js b/app/components/MultiModal/ConnectWallet/ConnectWalletScreens.js new file mode 100644 index 0000000..89c8d85 --- /dev/null +++ b/app/components/MultiModal/ConnectWallet/ConnectWalletScreens.js @@ -0,0 +1,61 @@ +import React, { useCallback, useMemo, useState } from 'react' + +import ConnectWallet from './ConnectWallet' +import ScreenProvidersWrapper from './ScreenProvidersWrapper' +import MultiModalScreens from '@/components/MultiModal/MultiModalScreens' +import ScreenErrorWrapper from './ScreenErrorWrapper' +import { useWallet } from '@providers/Wallet' + +function ConectWalletScreens({ onSuccess, onClose }) { + const [error, setError] = useState(null) + const { resetConnection } = useWallet() + + const handleOnError = useCallback(e => { + setError(e) + }, []) + + const handleTryAgain = useCallback(() => { + resetConnection() + setError(null) + }, [resetConnection]) + + const screens = useMemo(() => { + return [ + { + graphicHeader: false, + content: , + width: 550, + }, + { + graphicHeader: false, + content: ( + + ), + width: 550, + }, + { + graphicHeader: false, + content: , + width: 550, + }, + ] + }, [error, handleOnError, handleTryAgain, onClose, onSuccess]) + + const extendedScreens = useMemo(() => { + const allScreens = [] + + // Spread in our flow screens + if (screens) { + allScreens.push(...screens) + } + + return allScreens + }, [screens]) + + return +} + +export default ConectWalletScreens diff --git a/app/components/MultiModal/ConnectWallet/ScreenErrorWrapper.js b/app/components/MultiModal/ConnectWallet/ScreenErrorWrapper.js new file mode 100644 index 0000000..d3b96e8 --- /dev/null +++ b/app/components/MultiModal/ConnectWallet/ScreenErrorWrapper.js @@ -0,0 +1,17 @@ +import React, { useCallback } from 'react' +import { useMultiModal } from '@components/MultiModal/MultiModalProvider' + +import AccountModuleErrorScreen from '@/components/Account/ScreenError' + +function ScreenErrorWrapper({ error, onBack }) { + const { prev } = useMultiModal() + + const handleTryAgain = useCallback(() => { + onBack() + prev() + }, [onBack, prev]) + + return +} + +export default ScreenErrorWrapper diff --git a/app/components/MultiModal/ConnectWallet/ScreenProvidersWrapper.js b/app/components/MultiModal/ConnectWallet/ScreenProvidersWrapper.js new file mode 100644 index 0000000..6a46a7f --- /dev/null +++ b/app/components/MultiModal/ConnectWallet/ScreenProvidersWrapper.js @@ -0,0 +1,35 @@ +import React, { useCallback, useEffect } from 'react' +import { useWallet } from '@providers/Wallet' +import { useMultiModal } from '@components/MultiModal/MultiModalProvider' + +import ScreenProviders from '@components/Account/ScreenProviders' + +function ScreenProvidersWrapper({ onError, onSuccess }) { + const { account, connect, error } = useWallet() + const { next } = useMultiModal() + + const activate = useCallback( + async providerId => { + try { + await connect(providerId) + } catch (error) { + console.log('error ', error) + } + }, + [connect] + ) + + useEffect(() => { + if (error) { + onError(error) + return next() + } + if (account) { + return onSuccess() + } + }, [account, error, next, onError, onSuccess]) + + return +} + +export default ScreenProvidersWrapper diff --git a/app/components/MultiModal/MultiModal.tsx b/app/components/MultiModal/MultiModal.tsx new file mode 100644 index 0000000..2c11e84 --- /dev/null +++ b/app/components/MultiModal/MultiModal.tsx @@ -0,0 +1,53 @@ +import React, { useCallback, useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { noop } from '@blossom-labs/rosette-ui' +import { Inside } from 'use-inside' + +type MultiModalType = { + visible: boolean + onClose: () => void + onClosed: () => void + children: React.ReactNode +} + +function MultiModal({ visible, onClose, onClosed, children }: MultiModalType) { + const [render, setRender] = useState(visible) + + useEffect(() => { + if (visible) { + setRender(true) + } + }, [render, visible]) + + const handleOnClosed = useCallback(() => { + // Ensure react-spring has properly cleaned up state prior to unmount + setTimeout(() => { + onClosed() + setRender(false) + }) + }, [setRender, onClosed]) + + return ( + <> + {render && ( + + {children} + + )} + + ) +} + +MultiModal.propTypes = { + onClose: PropTypes.func, + onClosed: PropTypes.func, + visible: PropTypes.bool, + children: PropTypes.node, +} + +MultiModal.defaultProps = { + onClose: noop, + onClosed: noop, +} + +export default MultiModal diff --git a/app/components/MultiModal/MultiModalProvider.tsx b/app/components/MultiModal/MultiModalProvider.tsx new file mode 100644 index 0000000..9207113 --- /dev/null +++ b/app/components/MultiModal/MultiModalProvider.tsx @@ -0,0 +1,81 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { useContext, useEffect, useMemo, useCallback } from 'react' +import { useAccount } from "wagmi"; +import { noop } from '@blossom-labs/rosette-ui' +import { useSteps } from '~/hooks/useSteps' + +type State = { + currentScreen: any + close: () => void + direction: any + getScreen: any + next: any + prev: any + step: any +} + +const MultiModalContext = React.createContext({ + currentScreen: null, + // eslint-disable-next-line @typescript-eslint/no-empty-function + close: () => {}, + direction: null, + getScreen: null, + next: null, + prev: null, + step: null, +}) + +type MultiModalProviderType = { + screens: Array + onClose: () => void + children: React.ReactNode +} + +function MultiModalProvider({ + screens, + onClose, + children, +}: MultiModalProviderType) { + const [{ data: accountData }] = useAccount({ fetchEns: true }); + const { address, ens } = accountData || {}; + const { account } = useWallet() + const { direction, next, prev, setStep, step } = useSteps(screens.length) + const getScreen = useCallback((step) => screens[step], [screens]) + const currentScreen = useMemo(() => getScreen(step), [getScreen, step]) + + const handleClose = useCallback(() => onClose(), [onClose]) + + useEffect(() => { + setStep(0) + }, [account]) + + const multiModalState = useMemo( + () => ({ + // Prevent destructure errors if screens length is dynamically reduced below current index + currentScreen: currentScreen || {}, + close: handleClose, + direction, + getScreen, + next, + prev, + step, + }), + [currentScreen, direction, getScreen, next, prev, step, handleClose] + ) + + return ( + + {children} + + ) +} + +MultiModalProvider.defaultProps = { + onClose: noop, +} + +function useMultiModal() { + return useContext(MultiModalContext) +} + +export { MultiModalProvider, useMultiModal } diff --git a/app/components/MultiModal/MultiModalScreens.js b/app/components/MultiModal/MultiModalScreens.js new file mode 100644 index 0000000..7371545 --- /dev/null +++ b/app/components/MultiModal/MultiModalScreens.js @@ -0,0 +1,300 @@ +import React, { useCallback, useState } from 'react' +import PropTypes from 'prop-types' +import { Spring, Transition, animated } from 'react-spring/renderprops' +import { + ButtonIcon, + GU, + IconCross, + Modal, + RADIUS, + Root, + textStyle, + useLayout, + useTheme, + Viewport, +} from '@1hive/1hive-ui' +import { MultiModalProvider, useMultiModal } from './MultiModalProvider' +import { springs } from '../../style/springs' +import { useDisableAnimation } from '../../hooks/useDisableAnimation' +import { useInside } from 'use-inside' + +import headerBackground from '../../assets/modal-background.svg' + +const DEFAULT_MODAL_WIDTH = 80 * GU +const AnimatedDiv = animated.div + +function MultiModalScreens({ screens }) { + const [, { onClose, handleOnClosed, visible }] = useInside('MultiModal') + + return ( + + + + ) +} + +MultiModalScreens.propTypes = { + screens: PropTypes.arrayOf( + PropTypes.shape({ + content: PropTypes.node, + disableClose: PropTypes.bool, + graphicHeader: PropTypes.bool, + title: PropTypes.string, + width: PropTypes.number, + }) + ).isRequired, +} + +/* eslint-disable react/prop-types */ +function MultiModalFrame({ visible, onClosed }) { + const theme = useTheme() + const { currentScreen, close } = useMultiModal() + + const { + disableClose, + width: currentScreenWidth, + graphicHeader, + } = currentScreen + + const modalWidth = currentScreenWidth || DEFAULT_MODAL_WIDTH + + const handleModalClose = useCallback(() => { + if (!disableClose) { + close() + } + }, [disableClose, close]) + + return ( + + {({ width }) => { + // Apply a small gutter when matching the viewport width + const viewportWidth = width - 4 * GU + + return ( + + {({ width }) => { + return ( + div > div > div { + border-radius: ${2 * RADIUS}px !important; + } + `} + > +
+ {!disableClose && ( + + + + )} + + +
+
+ ) + }} +
+ ) + }} +
+ ) +} + +// We memoize this compontent to avoid excessive re-renders when animating +const MultiModalContent = React.memo(function ModalContent({ viewportWidth }) { + const theme = useTheme() + const { step, direction, getScreen } = useMultiModal() + const [applyStaticHeight, setApplyStaticHeight] = useState(false) + const [height, setHeight] = useState(null) + const [animationDisabled, enableAnimation] = useDisableAnimation() + const { layoutName } = useLayout() + + const smallMode = layoutName === 'small' + + const onStart = useCallback(() => { + enableAnimation() + + if (!animationDisabled) { + setApplyStaticHeight(true) + } + }, [animationDisabled, enableAnimation]) + + const renderScreen = useCallback( + (screen) => { + const { title, content, graphicHeader, width } = screen + const standardPadding = smallMode ? 3 * GU : 5 * GU + + return ( + <> + {graphicHeader ? ( +
+

+ {title} +

+
+ ) : ( + title && ( +
+

+ {title} +

+
+ ) + )} + + +
+ {content} +
+
+ + ) + }, + [smallMode, theme, viewportWidth] + ) + + return ( + + {({ height }) => ( + + + state === 'leave' ? springs.instant : springs.tight + } + items={step} + immediate={animationDisabled} + from={{ + opacity: 0, + transform: `translate3d(0, ${5 * GU * direction}px, 0)`, + }} + enter={{ + opacity: 1, + transform: 'translate3d(0, 0, 0)', + }} + leave={{ + position: 'absolute', + top: 0, + left: 0, + opacity: 0, + transform: `translate3d(0, ${5 * GU * -direction}px, 0)`, + }} + onRest={(_, status) => { + if (status === 'update') { + setApplyStaticHeight(false) + } + }} + onStart={onStart} + native + > + {(step) => (animProps) => { + const stepScreen = getScreen(step) + + return ( + <> + {stepScreen && ( + { + if (elt) { + setHeight(elt.clientHeight) + } + }} + style={{ + width: '100%', + ...animProps, + }} + > + {renderScreen(stepScreen)} + + )} + + ) + }} + + + )} + + ) +}) +/* eslint-enable react/prop-types */ + +export default MultiModalScreens diff --git a/app/hooks/useSteps.ts b/app/hooks/useSteps.ts new file mode 100644 index 0000000..12e2a17 --- /dev/null +++ b/app/hooks/useSteps.ts @@ -0,0 +1,88 @@ +import { useReducer, useEffect, useCallback } from 'react' + +export enum StepActionType { + SET = 'SET', + NEXT = 'NEXT', + PREV = 'PREV', + ADJUST_SIZE = 'ADJUST_SIZE', +} + +type State = { + step: number + direction: number +} + +type Action = { + type: StepActionType + value?: any + steps?: number +} + +function stepsReducer(state: State, action: Action) { + const { type, value, steps } = action + const { step } = state + const stepsCount = steps !== undefined ? steps - 1 : 0 + + let newStep = null + + if (type === StepActionType.SET) { + newStep = value + } + if (type === StepActionType.NEXT && step < stepsCount) { + newStep = step + 1 + } + if (type === StepActionType.PREV && step > 0) { + newStep = step - 1 + } + if (type === StepActionType.ADJUST_SIZE) { + newStep = step <= stepsCount ? step : stepsCount + } + + if (newStep !== null && step !== newStep) { + return { + step: newStep, + direction: newStep > step ? 1 : -1, + } + } + + return state +} + +// Simple hook to manage a given number of steps +export function useSteps(steps: number) { + const [state, dispatch] = useReducer(stepsReducer, { + step: 0, + direction: 0, + }) + + const { step, direction } = state + + // If the number of steps change, we need to remember the current step + // or use the closest value available + useEffect(() => { + dispatch({ type: StepActionType.ADJUST_SIZE, steps }) + }, [steps]) + + const setStep = useCallback( + value => { + dispatch({ type: StepActionType.SET, value, steps }) + }, + [steps] + ) + + const next = useCallback(() => { + dispatch({ type: StepActionType.NEXT, steps }) + }, [steps]) + + const prev = useCallback(() => { + dispatch({ type: StepActionType.PREV, steps }) + }, [steps]) + + return { + direction, + next, + prev, + setStep, + step, + } +} diff --git a/package.json b/package.json index 7fde2bc..4ded6c2 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lint": "eslint ./app" }, "dependencies": { + "@blossom-labs/rosette-react": "^0.1.1", "@blossom-labs/rosette-ui": "^0.0.1-alpha.6", "@netlify/functions": "^1.0.0", "@react-spring/web": "^9.4.4", diff --git a/yarn.lock b/yarn.lock index 8a2ac95..0ec3305 100644 --- a/yarn.lock +++ b/yarn.lock @@ -473,6 +473,29 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@blossom-labs/rosette-core@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@blossom-labs/rosette-core/-/rosette-core-0.1.1.tgz#ff8a4cc9e082387d3ac0ac9b596199b8b7b6686c" + integrity sha512-ptwezmVRmo6RR0W3jLe17IzzXX+6/GSj+qV3G8H69gF2/F2OIK7kr7w0I8RSM/koCS1t8hcVdY3LTNHSp9fRBg== + dependencies: + "@ethersproject/providers" "^5.6.4" + isomorphic-fetch "^3.0.0" + lru-cache "^7.9.0" + +"@blossom-labs/rosette-radspec@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@blossom-labs/rosette-radspec/-/rosette-radspec-0.1.1.tgz#968a84424005671b151c3fc8d798d10cda77ce45" + integrity sha512-K2cObo7Cgek4CtnRaJX6+5kF9hWJDfdsy8gY8WMXv3+betYpLZzK1Idrj4SvorxdY34q7ZqM0rL3KGPeJ7JtXg== + dependencies: + dayjs "^1.11.2" + +"@blossom-labs/rosette-react@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@blossom-labs/rosette-react/-/rosette-react-0.1.1.tgz#9d4bc1361f4031db9aa4b76b3a751680e94fc88f" + integrity sha512-0iTGxwZ0dL02d5cSkiGTKqBu5C5B0LYhxBlQ+8gzIqsslpGw01VHZtLqJh+lv9YvQq3eoQcqTRoxJHHe+V7uow== + dependencies: + "@blossom-labs/rosette" "^0.1.1" + "@blossom-labs/rosette-ui@^0.0.1-alpha.6": version "0.0.1-alpha.6" resolved "https://registry.yarnpkg.com/@blossom-labs/rosette-ui/-/rosette-ui-0.0.1-alpha.6.tgz#2c1e91d880117dcdec92b271b1892350bddbcf5a" @@ -496,6 +519,14 @@ recursive-copy "^2.0.9" use-inside "https://github.com/aragon/use-inside#16f321e499d8f89e9b0fb0999e2d21db2b119bbf" +"@blossom-labs/rosette@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@blossom-labs/rosette/-/rosette-0.1.1.tgz#5a5e55332fb427656778cf815fb1ce35461371b8" + integrity sha512-wAN+pGc53xtEKgGsHdTaLejKeY9yhscm0/klABOwcHLUSjB125SJbJNg5pl3EZT9Yz39x6uKZsK/RDnV5Fbotg== + dependencies: + "@blossom-labs/rosette-core" "^0.1.1" + "@blossom-labs/rosette-radspec" "^0.1.1" + "@emotion/is-prop-valid@^1.1.0": version "1.1.2" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.2.tgz#34ad6e98e871aa6f7a20469b602911b8b11b3a95" @@ -569,6 +600,19 @@ "@ethersproject/transactions" "^5.6.0" "@ethersproject/web" "^5.6.0" +"@ethersproject/abstract-provider@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.6.1.tgz#02ddce150785caf0c77fe036a0ebfcee61878c59" + integrity sha512-BxlIgogYJtp1FS8Muvj8YfdClk3unZH0vRMVX791Z9INBNT/kuACZ9GzaY1Y4yFq+YSy6/w4gzj3HCRKrK9hsQ== + dependencies: + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/networks" "^5.6.3" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/transactions" "^5.6.2" + "@ethersproject/web" "^5.6.1" + "@ethersproject/abstract-signer@5.6.0", "@ethersproject/abstract-signer@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.6.0.tgz#9cd7ae9211c2b123a3b29bf47aab17d4d016e3e7" @@ -580,6 +624,17 @@ "@ethersproject/logger" "^5.6.0" "@ethersproject/properties" "^5.6.0" +"@ethersproject/abstract-signer@^5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.6.2.tgz#491f07fc2cbd5da258f46ec539664713950b0b33" + integrity sha512-n1r6lttFBG0t2vNiI3HoWaS/KdOt8xyDjzlP2cuevlWLG6EX0OwcKLyG/Kp/cuwNxdy/ous+R/DEMdTUwWQIjQ== + dependencies: + "@ethersproject/abstract-provider" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/address@5.6.0", "@ethersproject/address@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.0.tgz#13c49836d73e7885fc148ad633afad729da25012" @@ -591,6 +646,17 @@ "@ethersproject/logger" "^5.6.0" "@ethersproject/rlp" "^5.6.0" +"@ethersproject/address@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.1.tgz#ab57818d9aefee919c5721d28cd31fd95eff413d" + integrity sha512-uOgF0kS5MJv9ZvCz7x6T2EXJSzotiybApn4XlOgoTX0xdtyVIJ7pF+6cGPxiEq/dpBiTfMiw7Yc81JcwhSYA0Q== + dependencies: + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/rlp" "^5.6.1" + "@ethersproject/base64@5.6.0", "@ethersproject/base64@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.6.0.tgz#a12c4da2a6fb86d88563216b0282308fc15907c9" @@ -598,6 +664,13 @@ dependencies: "@ethersproject/bytes" "^5.6.0" +"@ethersproject/base64@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.6.1.tgz#2c40d8a0310c9d1606c2c37ae3092634b41d87cb" + integrity sha512-qB76rjop6a0RIYYMiB4Eh/8n+Hxu2NIZm8S/Q7kNo5pmZfXhHGHmS4MinUainiBC54SCyRnwzL+KZjj8zbsSsw== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/basex@5.6.0", "@ethersproject/basex@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.6.0.tgz#9ea7209bf0a1c3ddc2a90f180c3a7f0d7d2e8a69" @@ -606,6 +679,14 @@ "@ethersproject/bytes" "^5.6.0" "@ethersproject/properties" "^5.6.0" +"@ethersproject/basex@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.6.1.tgz#badbb2f1d4a6f52ce41c9064f01eab19cc4c5305" + integrity sha512-a52MkVz4vuBXR06nvflPMotld1FJWSj2QT0985v7P/emPZO00PucFAkbcmq2vpVU7Ts7umKiSI6SppiLykVWsA== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/bignumber@5.6.0", "@ethersproject/bignumber@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.0.tgz#116c81b075c57fa765a8f3822648cf718a8a0e26" @@ -615,7 +696,16 @@ "@ethersproject/logger" "^5.6.0" bn.js "^4.11.9" -"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@^5.6.0": +"@ethersproject/bignumber@^5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.2.tgz#72a0717d6163fab44c47bcc82e0c550ac0315d66" + integrity sha512-v7+EEUbhGqT3XJ9LMPsKvXYHFc8eHxTowFCG/HgJErmq4XHJ2WR7aeyICg3uTOAQ7Icn0GFHAohXEhxQHq4Ubw== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + bn.js "^5.2.1" + +"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@^5.6.0", "@ethersproject/bytes@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7" integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g== @@ -629,6 +719,13 @@ dependencies: "@ethersproject/bignumber" "^5.6.0" +"@ethersproject/constants@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.6.1.tgz#e2e974cac160dd101cf79fdf879d7d18e8cb1370" + integrity sha512-QSq9WVnZbxXYFftrjSjZDUshp6/eKp6qrtdBtUCm0QxCV5z1fG/w3kdlcsjMCQuQHUnAclKoK7XpXMezhRDOLg== + dependencies: + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/contracts@5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.6.0.tgz#60f2cfc7addd99a865c6c8cfbbcec76297386067" @@ -659,6 +756,20 @@ "@ethersproject/properties" "^5.6.0" "@ethersproject/strings" "^5.6.0" +"@ethersproject/hash@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.6.1.tgz#224572ea4de257f05b4abf8ae58b03a67e99b0f4" + integrity sha512-L1xAHurbaxG8VVul4ankNX5HgQ8PNCTrnVXEiFnE9xoRnaUcgfD12tZINtDinSllxPLCtGwguQxJ5E6keE84pA== + dependencies: + "@ethersproject/abstract-signer" "^5.6.2" + "@ethersproject/address" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.1" + "@ethersproject/hdnode@5.6.0", "@ethersproject/hdnode@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.6.0.tgz#9dcbe8d629bbbcf144f2cae476337fe92d320998" @@ -704,6 +815,14 @@ "@ethersproject/bytes" "^5.6.0" js-sha3 "0.8.0" +"@ethersproject/keccak256@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.6.1.tgz#b867167c9b50ba1b1a92bccdd4f2d6bd168a91cc" + integrity sha512-bB7DQHCTRDooZZdL3lk9wpL0+XuG3XLGHLh3cePnybsO3V0rdCAOQGpn/0R3aODmnTOOkCATJiD2hnL+5bwthA== + dependencies: + "@ethersproject/bytes" "^5.6.1" + js-sha3 "0.8.0" + "@ethersproject/logger@5.6.0", "@ethersproject/logger@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.6.0.tgz#d7db1bfcc22fd2e4ab574cba0bb6ad779a9a3e7a" @@ -716,6 +835,13 @@ dependencies: "@ethersproject/logger" "^5.6.0" +"@ethersproject/networks@^5.6.3": + version "5.6.3" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.6.3.tgz#3ee3ab08f315b433b50c99702eb32e0cf31f899f" + integrity sha512-QZxRH7cA5Ut9TbXwZFiCyuPchdWi87ZtVNHWZd0R6YFgYtes2jQ3+bsslJ0WdyDe0i6QumqtoYqvY3rrQFRZOQ== + dependencies: + "@ethersproject/logger" "^5.6.0" + "@ethersproject/pbkdf2@5.6.0", "@ethersproject/pbkdf2@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.6.0.tgz#04fcc2d7c6bff88393f5b4237d906a192426685a" @@ -756,6 +882,32 @@ bech32 "1.1.4" ws "7.4.6" +"@ethersproject/providers@^5.6.4": + version "5.6.8" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.6.8.tgz#22e6c57be215ba5545d3a46cf759d265bb4e879d" + integrity sha512-Wf+CseT/iOJjrGtAOf3ck9zS7AgPmr2fZ3N97r4+YXN3mBePTG2/bJ8DApl9mVwYL+RpYbNxMEkEp4mPGdwG/w== + dependencies: + "@ethersproject/abstract-provider" "^5.6.1" + "@ethersproject/abstract-signer" "^5.6.2" + "@ethersproject/address" "^5.6.1" + "@ethersproject/base64" "^5.6.1" + "@ethersproject/basex" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/constants" "^5.6.1" + "@ethersproject/hash" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/networks" "^5.6.3" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/random" "^5.6.1" + "@ethersproject/rlp" "^5.6.1" + "@ethersproject/sha2" "^5.6.1" + "@ethersproject/strings" "^5.6.1" + "@ethersproject/transactions" "^5.6.2" + "@ethersproject/web" "^5.6.1" + bech32 "1.1.4" + ws "7.4.6" + "@ethersproject/random@5.6.0", "@ethersproject/random@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.6.0.tgz#1505d1ab6a250e0ee92f436850fa3314b2cb5ae6" @@ -764,6 +916,14 @@ "@ethersproject/bytes" "^5.6.0" "@ethersproject/logger" "^5.6.0" +"@ethersproject/random@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.6.1.tgz#66915943981bcd3e11bbd43733f5c3ba5a790255" + integrity sha512-/wtPNHwbmng+5yi3fkipA8YBT59DdkGRoC2vWk09Dci/q5DlgnMkhIycjHlavrvrjJBkFjO/ueLyT+aUDfc4lA== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/rlp@5.6.0", "@ethersproject/rlp@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.6.0.tgz#55a7be01c6f5e64d6e6e7edb6061aa120962a717" @@ -772,6 +932,14 @@ "@ethersproject/bytes" "^5.6.0" "@ethersproject/logger" "^5.6.0" +"@ethersproject/rlp@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.6.1.tgz#df8311e6f9f24dcb03d59a2bac457a28a4fe2bd8" + integrity sha512-uYjmcZx+DKlFUk7a5/W9aQVaoEC7+1MOBgNtvNg13+RnuUwT4F0zTovC0tmay5SmRslb29V1B7Y5KCri46WhuQ== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/sha2@5.6.0", "@ethersproject/sha2@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.6.0.tgz#364c4c11cc753bda36f31f001628706ebadb64d9" @@ -781,6 +949,15 @@ "@ethersproject/logger" "^5.6.0" hash.js "1.1.7" +"@ethersproject/sha2@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.6.1.tgz#211f14d3f5da5301c8972a8827770b6fd3e51656" + integrity sha512-5K2GyqcW7G4Yo3uenHegbXRPDgARpWUiXc6RiF7b6i/HXUoWlb7uCARh7BAHg7/qT/Q5ydofNwiZcim9qpjB6g== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + hash.js "1.1.7" + "@ethersproject/signing-key@5.6.0", "@ethersproject/signing-key@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.6.0.tgz#4f02e3fb09e22b71e2e1d6dc4bcb5dafa69ce042" @@ -793,6 +970,18 @@ elliptic "6.5.4" hash.js "1.1.7" +"@ethersproject/signing-key@^5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.6.2.tgz#8a51b111e4d62e5a62aee1da1e088d12de0614a3" + integrity sha512-jVbu0RuP7EFpw82vHcL+GP35+KaNruVAZM90GxgQnGqB6crhBqW/ozBfFvdeImtmb4qPko0uxXjn8l9jpn0cwQ== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + "@ethersproject/solidity@5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.6.0.tgz#64657362a596bf7f5630bdc921c07dd78df06dc3" @@ -814,6 +1003,15 @@ "@ethersproject/constants" "^5.6.0" "@ethersproject/logger" "^5.6.0" +"@ethersproject/strings@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.6.1.tgz#dbc1b7f901db822b5cafd4ebf01ca93c373f8952" + integrity sha512-2X1Lgk6Jyfg26MUnsHiT456U9ijxKUybz8IM1Vih+NJxYtXhmvKBcHOmvGqpFSVJ0nQ4ZCoIViR8XlRw1v/+Cw== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/constants" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/transactions@5.6.0", "@ethersproject/transactions@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.6.0.tgz#4b594d73a868ef6e1529a2f8f94a785e6791ae4e" @@ -829,6 +1027,21 @@ "@ethersproject/rlp" "^5.6.0" "@ethersproject/signing-key" "^5.6.0" +"@ethersproject/transactions@^5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.6.2.tgz#793a774c01ced9fe7073985bb95a4b4e57a6370b" + integrity sha512-BuV63IRPHmJvthNkkt9G70Ullx6AcM+SDc+a8Aw/8Yew6YwT51TcBKEp1P4oOQ/bP25I18JJr7rcFRgFtU9B2Q== + dependencies: + "@ethersproject/address" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/constants" "^5.6.1" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/rlp" "^5.6.1" + "@ethersproject/signing-key" "^5.6.2" + "@ethersproject/units@5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.6.0.tgz#e5cbb1906988f5740254a21b9ded6bd51e826d9c" @@ -870,6 +1083,17 @@ "@ethersproject/properties" "^5.6.0" "@ethersproject/strings" "^5.6.0" +"@ethersproject/web@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.6.1.tgz#6e2bd3ebadd033e6fe57d072db2b69ad2c9bdf5d" + integrity sha512-/vSyzaQlNXkO1WV+RneYKqCJwualcUdx/Z3gseVovZP0wIlOFcCE1hkRhKBH8ImKbGQbMl9EAAyJFrJu7V0aqA== + dependencies: + "@ethersproject/base64" "^5.6.1" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.1" + "@ethersproject/wordlists@5.6.0", "@ethersproject/wordlists@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.6.0.tgz#79e62c5276e091d8575f6930ba01a29218ded032" @@ -2329,6 +2553,11 @@ bn.js@^5.1.1, bn.js@^5.2.0: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== +bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + body-parser@1.19.2: version "1.19.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" @@ -2996,6 +3225,11 @@ damerau-levenshtein@^1.0.7: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +dayjs@^1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5" + integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw== + dayjs@^1.8.14: version "1.11.0" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.0.tgz#009bf7ef2e2ea2d5db2e6583d2d39a4b5061e805" @@ -5356,6 +5590,14 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isomorphic-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" + integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== + dependencies: + node-fetch "^2.6.1" + whatwg-fetch "^3.4.1" + it-all@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/it-all/-/it-all-1.0.6.tgz#852557355367606295c4c3b7eff0136f07749335" @@ -5747,6 +5989,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.9.0: + version "7.10.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.10.1.tgz#db577f42a94c168f676b638d15da8fb073448cab" + integrity sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A== + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -8831,6 +9078,11 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= +whatwg-fetch@^3.4.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" From 93056048bb0f6b203543863186d60c9110422eeb Mon Sep 17 00:00:00 2001 From: Gabi Date: Thu, 2 Jun 2022 20:39:46 -0300 Subject: [PATCH 2/4] chore: continu with draft --- app/components/ModalFlows/ModalFlowBase.tsx | 179 ++++----- .../MultiModal/ConnectWallet/ConnectWallet.js | 77 ---- .../ConnectWallet/ConnectWalletScreens.js | 61 ---- .../ConnectWallet/ScreenErrorWrapper.js | 17 - .../ConnectWallet/ScreenProvidersWrapper.js | 35 -- app/components/MultiModal/MultiModal.tsx | 40 +- .../MultiModal/MultiModalProvider.tsx | 59 ++- .../MultiModal/MultiModalScreens.js | 300 --------------- .../MultiModal/MultiModalScreens.tsx | 342 ++++++++++++++++++ app/components/Stepper/Step/Divider.tsx | 25 ++ app/components/Stepper/Step/Illustration.tsx | 47 +++ app/components/Stepper/Step/StatusVisual.tsx | 236 ++++++++++++ app/components/Stepper/Step/Step.tsx | 226 ++++++++++++ app/components/Stepper/Stepper.tsx | 275 ++++++++++++++ .../Stepper/stepper-descriptions.ts | 19 + app/components/Stepper/stepper-statuses.ts | 13 + app/components/Stepper/useStepperLayout.ts | 38 ++ app/hooks/useDisableAnimation.ts | 19 + app/hooks/useMounted.ts | 16 + package.json | 2 + yarn.lock | 17 + 21 files changed, 1401 insertions(+), 642 deletions(-) delete mode 100644 app/components/MultiModal/ConnectWallet/ConnectWallet.js delete mode 100644 app/components/MultiModal/ConnectWallet/ConnectWalletScreens.js delete mode 100644 app/components/MultiModal/ConnectWallet/ScreenErrorWrapper.js delete mode 100644 app/components/MultiModal/ConnectWallet/ScreenProvidersWrapper.js delete mode 100644 app/components/MultiModal/MultiModalScreens.js create mode 100644 app/components/MultiModal/MultiModalScreens.tsx create mode 100644 app/components/Stepper/Step/Divider.tsx create mode 100644 app/components/Stepper/Step/Illustration.tsx create mode 100644 app/components/Stepper/Step/StatusVisual.tsx create mode 100644 app/components/Stepper/Step/Step.tsx create mode 100644 app/components/Stepper/Stepper.tsx create mode 100644 app/components/Stepper/stepper-descriptions.ts create mode 100644 app/components/Stepper/stepper-statuses.ts create mode 100644 app/components/Stepper/useStepperLayout.ts create mode 100644 app/hooks/useDisableAnimation.ts create mode 100644 app/hooks/useMounted.ts diff --git a/app/components/ModalFlows/ModalFlowBase.tsx b/app/components/ModalFlows/ModalFlowBase.tsx index ba2809f..9abd653 100644 --- a/app/components/ModalFlows/ModalFlowBase.tsx +++ b/app/components/ModalFlows/ModalFlowBase.tsx @@ -1,43 +1,46 @@ -import React, { useEffect, useMemo } from 'react' -import { keyframes } from 'styled-components' - -import { GU, useTheme } from '@blossom-labs/rosette-ui' +import React, { useEffect, useMemo } from "react"; +import { useSigner } from "wagmi"; import { LoadingRing } from "../LoadingRing"; -import { useMultiModal } from '@components/MultiModal/MultiModalProvider' -import MultiModalScreens from '@components/MultiModal/MultiModalScreens' -import Stepper from '@components/Stepper/Stepper' +import { useMultiModal } from "../MultiModal/MultiModalProvider"; +import MultiModalScreens from "../MultiModal/MultiModalScreens"; +import Stepper from "../Stepper/Stepper"; -import { useActivity } from '@providers/ActivityProvider' -import { useWallet } from '@providers/Wallet' +// import { useActivity } from '@providers/ActivityProvider' -import { setAccountSetting } from '@/local-settings' -import { TransactionType } from '@/hooks/constants' +type TransactionType = { + data: any; + from: string | undefined; + to: string | undefined; + description?: string; + type?: string; + gasLimit?: number; +}; -const INDEX_NUMBER = ['First', 'Second', 'Third', 'Fourth', 'Fifth'] +const INDEX_NUMBER = ["First", "Second", "Third", "Fourth", "Fifth"]; type ModalFlowBaseType = { - loading: boolean - screens: Array - transactions: Array - onComplete: () => void - onCompleteActions: React.ReactNode - frontLoad?: boolean - transactionTitle?: string -} + loading: boolean; + screens: Array; + transactions: Array; + onComplete: () => void; + onCompleteActions: any; + frontLoad?: boolean; + transactionTitle?: string; +}; type HandleSignParamsType = { - setSuccess: () => void - setWorking: () => void - setError: () => void - setHash: (hash: any) => void -} + setSuccess: () => void; + setWorking: () => void; + setError: () => void; + setHash: (hash: any) => void; +}; type TransactionStepsType = { - title: string - handleSign: (params: HandleSignParamsType) => Promise -} -type ArrayTransactionStepsType = Array | null + title: string; + handleSign: (params: HandleSignParamsType) => Promise; +}; +type ArrayTransactionStepsType = Array | null; function ModalFlowBase({ loading, @@ -46,11 +49,10 @@ function ModalFlowBase({ onComplete, onCompleteActions, frontLoad = true, - transactionTitle = 'Create transaction', + transactionTitle = "Create transaction", }: ModalFlowBaseType) { - const { addActivity } = useActivity() - const { account, chainId, ethers } = useWallet() - const signer = useMemo(() => ethers.getSigner(), [ethers]) + // const { addActivity } = useActivity(); + const [{ data: signer }] = useSigner(); const transactionSteps: ArrayTransactionStepsType = useMemo( () => @@ -59,8 +61,8 @@ function ModalFlowBase({ const title = transaction.description ? transaction.description : transactions.length === 1 - ? 'Sign transaction' - : `${INDEX_NUMBER[index]} transaction` + ? "Sign transaction" + : `${INDEX_NUMBER[index]} transaction`; return { // TODO: Add titles from description @@ -77,48 +79,51 @@ function ModalFlowBase({ to: transaction.to, data: transaction.data, gasLimit: transaction.gasLimit, - } - const tx = await signer.sendTransaction(trx) + }; - await addActivity( - tx, - transaction.type, - transaction.description - ) - setHash(tx.hash) + // TODO: use useTransaction hook + const tx = await signer!.sendTransaction(trx); - setWorking() + // await addActivity( + // tx, + // transaction.type, + // transaction.description + // ); + setHash(tx.hash); + + setWorking(); // We need to wait for pre-transactions to mine before asking for the next signature // TODO: Provide a better user experience than waiting on all transactions - await tx.wait() + await tx.wait(); - setAccountSetting('lastTxHash', account, chainId, tx.hash) + // setAccountSetting("lastTxHash", account, chainId, tx.hash); - setSuccess() + setSuccess(); } catch (err) { - console.error(err) - setError() + console.error(err); + setError(); } }, - } + }; }) : null, - [addActivity, transactions, signer] - ) + [transactions, signer] // addActivity + ); + const extendedScreens = useMemo(() => { - const allScreens = [] + const allScreens = []; // Add loading screen as first item if enabled if (frontLoad) { allScreens.push({ content: , - }) + }); } // Spread in our flow screens if (screens) { - allScreens.push(...screens) + allScreens.push(...screens); } // Apply transaction singing at the end @@ -133,10 +138,10 @@ function ModalFlowBase({ onCompleteActions={onCompleteActions} /> ), - }) + }); } - return allScreens + return allScreens; }, [ frontLoad, loading, @@ -146,73 +151,43 @@ function ModalFlowBase({ transactionTitle, onComplete, onCompleteActions, - ]) + ]); - return + return ; } function LoadingScreen({ loading }: { loading: boolean }) { - const theme = useTheme() - const { next } = useMultiModal() + const { next } = useMultiModal(); useEffect(() => { - let timeout: number + let timeout: number; if (!loading) { // Provide a minimum appearance duration to avoid visual confusion on very fast requests timeout = window.setTimeout(() => { - next() - }, 100) + next(); + }, 100); } return () => { - clearTimeout(timeout) - } - }, [loading, next]) - - return ( -
-
- -
-
- ) + clearTimeout(timeout); + }; + }, [loading, next]); + + return ; } function modalWidthFromCount(count: number) { if (count >= 3) { - return 865 + return 865; } if (count === 2) { - return 700 + return 700; } // Modal will fallback to the default - return null + return null; } -export default ModalFlowBase +export default ModalFlowBase; diff --git a/app/components/MultiModal/ConnectWallet/ConnectWallet.js b/app/components/MultiModal/ConnectWallet/ConnectWallet.js deleted file mode 100644 index 07d79e2..0000000 --- a/app/components/MultiModal/ConnectWallet/ConnectWallet.js +++ /dev/null @@ -1,77 +0,0 @@ -import React, { useCallback } from 'react' -import { GU, textStyle, Button } from '@1hive/1hive-ui' -import { useMultiModal } from '@components/MultiModal/MultiModalProvider' -import connectionError from '@/assets/connection-error.svg' - -function ConectWallet({ onDismiss }) { - const { next } = useMultiModal() - const handleOnConnect = useCallback(() => { - next() - }, [next]) - return ( -
-

- Connect your account -

-

- You need to connect your account to create a garden -

- -
- - -
-
- ) -} - -export default ConectWallet diff --git a/app/components/MultiModal/ConnectWallet/ConnectWalletScreens.js b/app/components/MultiModal/ConnectWallet/ConnectWalletScreens.js deleted file mode 100644 index 89c8d85..0000000 --- a/app/components/MultiModal/ConnectWallet/ConnectWalletScreens.js +++ /dev/null @@ -1,61 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react' - -import ConnectWallet from './ConnectWallet' -import ScreenProvidersWrapper from './ScreenProvidersWrapper' -import MultiModalScreens from '@/components/MultiModal/MultiModalScreens' -import ScreenErrorWrapper from './ScreenErrorWrapper' -import { useWallet } from '@providers/Wallet' - -function ConectWalletScreens({ onSuccess, onClose }) { - const [error, setError] = useState(null) - const { resetConnection } = useWallet() - - const handleOnError = useCallback(e => { - setError(e) - }, []) - - const handleTryAgain = useCallback(() => { - resetConnection() - setError(null) - }, [resetConnection]) - - const screens = useMemo(() => { - return [ - { - graphicHeader: false, - content: , - width: 550, - }, - { - graphicHeader: false, - content: ( - - ), - width: 550, - }, - { - graphicHeader: false, - content: , - width: 550, - }, - ] - }, [error, handleOnError, handleTryAgain, onClose, onSuccess]) - - const extendedScreens = useMemo(() => { - const allScreens = [] - - // Spread in our flow screens - if (screens) { - allScreens.push(...screens) - } - - return allScreens - }, [screens]) - - return -} - -export default ConectWalletScreens diff --git a/app/components/MultiModal/ConnectWallet/ScreenErrorWrapper.js b/app/components/MultiModal/ConnectWallet/ScreenErrorWrapper.js deleted file mode 100644 index d3b96e8..0000000 --- a/app/components/MultiModal/ConnectWallet/ScreenErrorWrapper.js +++ /dev/null @@ -1,17 +0,0 @@ -import React, { useCallback } from 'react' -import { useMultiModal } from '@components/MultiModal/MultiModalProvider' - -import AccountModuleErrorScreen from '@/components/Account/ScreenError' - -function ScreenErrorWrapper({ error, onBack }) { - const { prev } = useMultiModal() - - const handleTryAgain = useCallback(() => { - onBack() - prev() - }, [onBack, prev]) - - return -} - -export default ScreenErrorWrapper diff --git a/app/components/MultiModal/ConnectWallet/ScreenProvidersWrapper.js b/app/components/MultiModal/ConnectWallet/ScreenProvidersWrapper.js deleted file mode 100644 index 6a46a7f..0000000 --- a/app/components/MultiModal/ConnectWallet/ScreenProvidersWrapper.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useCallback, useEffect } from 'react' -import { useWallet } from '@providers/Wallet' -import { useMultiModal } from '@components/MultiModal/MultiModalProvider' - -import ScreenProviders from '@components/Account/ScreenProviders' - -function ScreenProvidersWrapper({ onError, onSuccess }) { - const { account, connect, error } = useWallet() - const { next } = useMultiModal() - - const activate = useCallback( - async providerId => { - try { - await connect(providerId) - } catch (error) { - console.log('error ', error) - } - }, - [connect] - ) - - useEffect(() => { - if (error) { - onError(error) - return next() - } - if (account) { - return onSuccess() - } - }, [account, error, next, onError, onSuccess]) - - return -} - -export default ScreenProvidersWrapper diff --git a/app/components/MultiModal/MultiModal.tsx b/app/components/MultiModal/MultiModal.tsx index 2c11e84..b9025db 100644 --- a/app/components/MultiModal/MultiModal.tsx +++ b/app/components/MultiModal/MultiModal.tsx @@ -1,31 +1,31 @@ -import React, { useCallback, useState, useEffect } from 'react' -import PropTypes from 'prop-types' -import { noop } from '@blossom-labs/rosette-ui' -import { Inside } from 'use-inside' +import React, { useCallback, useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import { noop } from "@blossom-labs/rosette-ui"; +import { Inside } from "use-inside"; type MultiModalType = { - visible: boolean - onClose: () => void - onClosed: () => void - children: React.ReactNode -} + visible: boolean; + onClose: () => void; + onClosed: () => void; + children: React.ReactNode; +}; function MultiModal({ visible, onClose, onClosed, children }: MultiModalType) { - const [render, setRender] = useState(visible) + const [render, setRender] = useState(visible); useEffect(() => { if (visible) { - setRender(true) + setRender(true); } - }, [render, visible]) + }, [render, visible]); const handleOnClosed = useCallback(() => { // Ensure react-spring has properly cleaned up state prior to unmount setTimeout(() => { - onClosed() - setRender(false) - }) - }, [setRender, onClosed]) + onClosed(); + setRender(false); + }); + }, [setRender, onClosed]); return ( <> @@ -35,7 +35,7 @@ function MultiModal({ visible, onClose, onClosed, children }: MultiModalType) { )} - ) + ); } MultiModal.propTypes = { @@ -43,11 +43,11 @@ MultiModal.propTypes = { onClosed: PropTypes.func, visible: PropTypes.bool, children: PropTypes.node, -} +}; MultiModal.defaultProps = { onClose: noop, onClosed: noop, -} +}; -export default MultiModal +export default MultiModal; diff --git a/app/components/MultiModal/MultiModalProvider.tsx b/app/components/MultiModal/MultiModalProvider.tsx index 9207113..00aa35a 100644 --- a/app/components/MultiModal/MultiModalProvider.tsx +++ b/app/components/MultiModal/MultiModalProvider.tsx @@ -1,18 +1,18 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import React, { useContext, useEffect, useMemo, useCallback } from 'react' +import React, { useContext, useEffect, useMemo, useCallback } from "react"; import { useAccount } from "wagmi"; -import { noop } from '@blossom-labs/rosette-ui' -import { useSteps } from '~/hooks/useSteps' +import { noop } from "@blossom-labs/rosette-ui"; +import { useSteps } from "~/hooks/useSteps"; type State = { - currentScreen: any - close: () => void - direction: any - getScreen: any - next: any - prev: any - step: any -} + currentScreen: any; + close: () => void; + direction: any; + getScreen: any; + next: any; + prev: any; + step: any; +}; const MultiModalContext = React.createContext({ currentScreen: null, @@ -23,31 +23,30 @@ const MultiModalContext = React.createContext({ next: null, prev: null, step: null, -}) +}); type MultiModalProviderType = { - screens: Array - onClose: () => void - children: React.ReactNode -} + screens: Array; + onClose: () => void; + children: React.ReactNode; +}; function MultiModalProvider({ screens, onClose, children, }: MultiModalProviderType) { - const [{ data: accountData }] = useAccount({ fetchEns: true }); - const { address, ens } = accountData || {}; - const { account } = useWallet() - const { direction, next, prev, setStep, step } = useSteps(screens.length) - const getScreen = useCallback((step) => screens[step], [screens]) - const currentScreen = useMemo(() => getScreen(step), [getScreen, step]) + const [{ data: accountData }] = useAccount(); + const { address } = accountData || {}; + const { direction, next, prev, setStep, step } = useSteps(screens.length); + const getScreen = useCallback((step) => screens[step], [screens]); + const currentScreen = useMemo(() => getScreen(step), [getScreen, step]); - const handleClose = useCallback(() => onClose(), [onClose]) + const handleClose = useCallback(() => onClose(), [onClose]); useEffect(() => { - setStep(0) - }, [account]) + setStep(0); + }, [address]); const multiModalState = useMemo( () => ({ @@ -61,21 +60,21 @@ function MultiModalProvider({ step, }), [currentScreen, direction, getScreen, next, prev, step, handleClose] - ) + ); return ( {children} - ) + ); } MultiModalProvider.defaultProps = { onClose: noop, -} +}; function useMultiModal() { - return useContext(MultiModalContext) + return useContext(MultiModalContext); } -export { MultiModalProvider, useMultiModal } +export { MultiModalProvider, useMultiModal }; diff --git a/app/components/MultiModal/MultiModalScreens.js b/app/components/MultiModal/MultiModalScreens.js deleted file mode 100644 index 7371545..0000000 --- a/app/components/MultiModal/MultiModalScreens.js +++ /dev/null @@ -1,300 +0,0 @@ -import React, { useCallback, useState } from 'react' -import PropTypes from 'prop-types' -import { Spring, Transition, animated } from 'react-spring/renderprops' -import { - ButtonIcon, - GU, - IconCross, - Modal, - RADIUS, - Root, - textStyle, - useLayout, - useTheme, - Viewport, -} from '@1hive/1hive-ui' -import { MultiModalProvider, useMultiModal } from './MultiModalProvider' -import { springs } from '../../style/springs' -import { useDisableAnimation } from '../../hooks/useDisableAnimation' -import { useInside } from 'use-inside' - -import headerBackground from '../../assets/modal-background.svg' - -const DEFAULT_MODAL_WIDTH = 80 * GU -const AnimatedDiv = animated.div - -function MultiModalScreens({ screens }) { - const [, { onClose, handleOnClosed, visible }] = useInside('MultiModal') - - return ( - - - - ) -} - -MultiModalScreens.propTypes = { - screens: PropTypes.arrayOf( - PropTypes.shape({ - content: PropTypes.node, - disableClose: PropTypes.bool, - graphicHeader: PropTypes.bool, - title: PropTypes.string, - width: PropTypes.number, - }) - ).isRequired, -} - -/* eslint-disable react/prop-types */ -function MultiModalFrame({ visible, onClosed }) { - const theme = useTheme() - const { currentScreen, close } = useMultiModal() - - const { - disableClose, - width: currentScreenWidth, - graphicHeader, - } = currentScreen - - const modalWidth = currentScreenWidth || DEFAULT_MODAL_WIDTH - - const handleModalClose = useCallback(() => { - if (!disableClose) { - close() - } - }, [disableClose, close]) - - return ( - - {({ width }) => { - // Apply a small gutter when matching the viewport width - const viewportWidth = width - 4 * GU - - return ( - - {({ width }) => { - return ( - div > div > div { - border-radius: ${2 * RADIUS}px !important; - } - `} - > -
- {!disableClose && ( - - - - )} - - -
-
- ) - }} -
- ) - }} -
- ) -} - -// We memoize this compontent to avoid excessive re-renders when animating -const MultiModalContent = React.memo(function ModalContent({ viewportWidth }) { - const theme = useTheme() - const { step, direction, getScreen } = useMultiModal() - const [applyStaticHeight, setApplyStaticHeight] = useState(false) - const [height, setHeight] = useState(null) - const [animationDisabled, enableAnimation] = useDisableAnimation() - const { layoutName } = useLayout() - - const smallMode = layoutName === 'small' - - const onStart = useCallback(() => { - enableAnimation() - - if (!animationDisabled) { - setApplyStaticHeight(true) - } - }, [animationDisabled, enableAnimation]) - - const renderScreen = useCallback( - (screen) => { - const { title, content, graphicHeader, width } = screen - const standardPadding = smallMode ? 3 * GU : 5 * GU - - return ( - <> - {graphicHeader ? ( -
-

- {title} -

-
- ) : ( - title && ( -
-

- {title} -

-
- ) - )} - - -
- {content} -
-
- - ) - }, - [smallMode, theme, viewportWidth] - ) - - return ( - - {({ height }) => ( - - - state === 'leave' ? springs.instant : springs.tight - } - items={step} - immediate={animationDisabled} - from={{ - opacity: 0, - transform: `translate3d(0, ${5 * GU * direction}px, 0)`, - }} - enter={{ - opacity: 1, - transform: 'translate3d(0, 0, 0)', - }} - leave={{ - position: 'absolute', - top: 0, - left: 0, - opacity: 0, - transform: `translate3d(0, ${5 * GU * -direction}px, 0)`, - }} - onRest={(_, status) => { - if (status === 'update') { - setApplyStaticHeight(false) - } - }} - onStart={onStart} - native - > - {(step) => (animProps) => { - const stepScreen = getScreen(step) - - return ( - <> - {stepScreen && ( - { - if (elt) { - setHeight(elt.clientHeight) - } - }} - style={{ - width: '100%', - ...animProps, - }} - > - {renderScreen(stepScreen)} - - )} - - ) - }} - - - )} - - ) -}) -/* eslint-enable react/prop-types */ - -export default MultiModalScreens diff --git a/app/components/MultiModal/MultiModalScreens.tsx b/app/components/MultiModal/MultiModalScreens.tsx new file mode 100644 index 0000000..e25cfc5 --- /dev/null +++ b/app/components/MultiModal/MultiModalScreens.tsx @@ -0,0 +1,342 @@ +import React, { useCallback, useState } from "react"; +import PropTypes from "prop-types"; +import styled from "styled-components"; +import { Spring, Transition, animated } from "@react-spring/web"; +import { + ButtonIcon, + GU, + IconCross, + Modal, + RADIUS, + Root, + textStyle, + useLayout, + useTheme, + Viewport, + springs as baseSprings, +} from "@blossom-labs/rosette-ui"; +import { useInside } from "use-inside"; +import { useDisableAnimation } from "~/hooks/useDisableAnimation"; +import { MultiModalProvider, useMultiModal } from "./MultiModalProvider"; + +const DEFAULT_MODAL_WIDTH = 80 * GU; +const AnimatedDiv = animated.div; + +const springs = { + ...baseSprings, + gentle: { mass: 1, tension: 200, friction: 20 }, + tight: { mass: 0.6, tension: 500, friction: 40 }, +}; + +type MultiModalScreensType = { + screens: Array; +}; + +type MultiModalDataType = { + visible: boolean; + onClose: () => void; + handleOnClosed: () => void; +}; + +function MultiModalScreens({ screens }: MultiModalScreensType) { + const [, { onClose, handleOnClosed, visible }] = useInside("MultiModal") as [ + boolean, + MultiModalDataType + ]; + + return ( + + + + ); +} + +MultiModalScreens.propTypes = { + screens: PropTypes.arrayOf( + PropTypes.shape({ + content: PropTypes.node, + disableClose: PropTypes.bool, + graphicHeader: PropTypes.bool, + title: PropTypes.string, + width: PropTypes.number, + }) + ).isRequired, +}; + +type MultiModalFrameType = { + visible: boolean; + onClosed: () => void; +}; + +/* eslint-disable react/prop-types */ +function MultiModalFrame({ visible, onClosed }: MultiModalFrameType) { + const theme = useTheme(); + const { currentScreen, close } = useMultiModal(); + + const { + disableClose, + width: currentScreenWidth, + graphicHeader, + } = currentScreen; + + const modalWidth = currentScreenWidth || DEFAULT_MODAL_WIDTH; + + const handleModalClose = useCallback(() => { + if (!disableClose) { + close(); + } + }, [disableClose, close]); + + return ( + + {({ width }: { width: number }) => { + // Apply a small gutter when matching the viewport width + const viewportWidth = width - 4 * GU; + + return ( + + {({ width }) => { + return ( + div > div > div { + border-radius: ${2 * RADIUS}px !important; + } + `} + > +
+ {!disableClose && ( + + + + )} + + +
+
+ ); + }} +
+ ); + }} +
+ ); +} + +// We memoize this compontent to avoid excessive re-renders when animating +const MultiModalContent = React.memo(function ModalContent({ + viewportWidth, +}: { + viewportWidth: number; +}) { + const theme = useTheme(); + const { step, direction, getScreen } = useMultiModal(); + const [applyStaticHeight, setApplyStaticHeight] = useState(false); + const [height, setHeight] = useState(0); + const [animationDisabled, enableAnimation] = useDisableAnimation(); + const { layoutName } = useLayout(); + + const smallMode = layoutName === "small"; + + const onStart = useCallback(() => { + enableAnimation(); + + if (!animationDisabled) { + setApplyStaticHeight(true); + } + }, [animationDisabled, enableAnimation]); + + const renderScreen = useCallback( + (screen) => { + const { title, content, graphicHeader, width } = screen; + const standardPadding = smallMode ? 3 * GU : 5 * GU; + + return ( + <> + {graphicHeader ? ( + + {title} + + ) : ( + title && ( + + {title} + + ) + )} + + + + {content} + + + + ); + }, + [smallMode, theme, viewportWidth] + ); + + return ( + + {({ height }) => ( + + + //TransitionPhase LEAVE + state === 3 ? springs.instant : springs.tight + } + items={step} + immediate={animationDisabled} + from={{ + opacity: 0, + transform: `translate3d(0, ${5 * GU * direction}px, 0)`, + }} + enter={{ + opacity: 1, + transform: "translate3d(0, 0, 0)", + }} + leave={{ + position: "absolute", + top: 0, + left: 0, + opacity: 0, + transform: `translate3d(0, ${5 * GU * -direction}px, 0)`, + }} + onRest={(_: any, status: any) => { + if (status === "update") { + setApplyStaticHeight(false); + } + }} + onStart={onStart} + > + {(step) => (animProps: any) => { + const stepScreen = getScreen(step); + + return ( + <> + {stepScreen && ( + { + if (elt) { + setHeight(elt.clientHeight); + } + }} + style={{ + width: "100%", + ...animProps, + }} + > + {renderScreen(stepScreen)} + + )} + + ); + }} + + + )} + + ); +}); +/* eslint-enable react/prop-types */ + +export default MultiModalScreens; + +const HeaderContainer = styled.div<{ + standardPadding: number; + smallMode: boolean; +}>` + position: relative; + overflow: hidden; + ${({ standardPadding }) => + `padding: ${1.5 * GU}px ${standardPadding}px ${ + 1.5 * GU + }px ${standardPadding}px;`} + margin-bottom: ${({ smallMode }) => (smallMode ? 3 * GU : 5 * GU)}px; +`; + +const StyledH1 = styled.h1<{ + smallMode: boolean; +}>` + position: relative; + z-index: 1; + ${({ smallMode }) => (smallMode ? textStyle("title3") : textStyle("title2"))}; + font-weight: 600; + color: ${({ theme }) => theme.overlay}; +`; + +const TitleContainer = styled.div<{ + standardPadding: number; + smallMode: boolean; +}>` + padding: ${({ smallMode, standardPadding }) => + `smallMode ? 3 * GU : 5 * GU}px + ${standardPadding}px ${smallMode ? 1.5 * GU : 2.5 * GU}px + ${standardPadding}px;`}; +`; + +const StyledTitleH1 = styled.h1<{ + smallMode: boolean; +}>` + ${({ smallMode }) => (smallMode ? textStyle("title3") : textStyle("title2"))}; + margin-top: -${0.5 * GU}px; +`; + +const StyledContent = styled.div<{ + title: any; + width: number; + standardPadding: number; +}>` + /* For better performance we avoid reflowing long text between screen changes by matching the screen width with the modal width */ + width: ${({ width }) => `${width}px;`} + ${({ title, standardPadding }) => `padding: ${ + title ? 0 : standardPadding + }px ${standardPadding}px + ${standardPadding}px ${standardPadding}px;`}; +`; diff --git a/app/components/Stepper/Step/Divider.tsx b/app/components/Stepper/Step/Divider.tsx new file mode 100644 index 0000000..d072feb --- /dev/null +++ b/app/components/Stepper/Step/Divider.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import PropTypes from "prop-types"; + +function Divider({ color, ...props }: { color: string }) { + return ( + + + + + + + + + + + + + ); +} + +Divider.propTypes = { + color: PropTypes.string, +}; + +export default Divider; diff --git a/app/components/Stepper/Step/Illustration.tsx b/app/components/Stepper/Step/Illustration.tsx new file mode 100644 index 0000000..7308e32 --- /dev/null +++ b/app/components/Stepper/Step/Illustration.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import styled from "styled-components"; +import { IndividualStepTypes } from "../stepper-statuses"; +import blockIcon from "@assets/blockIcon.svg"; +import signRequestFailIllustration from "@assets/signRequestFail.svg"; +import signRequestSuccessIllustration from "@assets/signRequestSuccess.svg"; +import trxBeingMinedIllustration from "@assets/trxBeingMined.svg"; + +const illustrations = { + [IndividualStepTypes.Working]: trxBeingMinedIllustration, + [IndividualStepTypes.Success]: signRequestSuccessIllustration, + [IndividualStepTypes.Error]: signRequestFailIllustration, +}; + +function Illustration({ + status, + index, +}: { + status: IndividualStepTypes; + index: number; +}) { + return ( + <> + {status === IndividualStepTypes.Prompting ? ( + + + + ) : ( + // @ts-expect-error + + )} + + ); +} + +export default Illustration; + +const StyledDiv = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + border-radius: 100%; + background-color: ${({ theme }) => theme.contentSecondary}; + color: ${({ theme }) => theme.positiveContent}; +`; diff --git a/app/components/Stepper/Step/StatusVisual.tsx b/app/components/Stepper/Step/StatusVisual.tsx new file mode 100644 index 0000000..b239e1e --- /dev/null +++ b/app/components/Stepper/Step/StatusVisual.tsx @@ -0,0 +1,236 @@ +import React, { useMemo } from "react"; +import PropTypes from "prop-types"; +import { Transition, animated } from "@react-spring/web"; +import { css, keyframes } from "styled-components"; +import { + GU, + textStyle, + IconCross, + IconCheck, + useTheme, + springs as baseSprings, +} from "@blossom-labs/rosette-ui"; +import { IndividualStepTypes } from "../stepper-statuses"; +import Illustration from "./Illustration"; +import { useDisableAnimation } from "~/hooks/useDisableAnimation"; + +const STATUS_ICONS = { + [IndividualStepTypes.Error]: IconCross, + [IndividualStepTypes.Success]: IconCheck, +}; + +const AnimatedDiv = animated.div; + +const springs = { + ...baseSprings, + gentle: { mass: 1, tension: 200, friction: 20 }, + tight: { mass: 0.6, tension: 500, friction: 40 }, +}; + +const spinAnimation = css` + mask-image: linear-gradient(35deg, rgba(0, 0, 0, 0.1) 10%, rgba(0, 0, 0, 1)); + animation: ${keyframes` + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } + `} 1.25s linear infinite; +`; + +const pulseAnimation = css` + animation: ${keyframes` + from { + opacity: 1; + } + + to { + opacity: 0.1; + } + `} 0.75s linear alternate infinite; +`; + +function StatusVisual({ + status, + color, + number, + withoutFirstStep, + ...props +}: any) { + const theme = useTheme(); + const [animationDisabled, enableAnimation] = useDisableAnimation(); + + const [statusIcon, illustration] = useMemo(() => { + // @ts-expect-error + const Icon = STATUS_ICONS[status]; + + return [ + Icon && , + , + ]; + }, [status, number, withoutFirstStep]); + + return ( +
+
+
+
+ + // TransitionPhase ENTER + state === 1 ? springs.gentle : springs.instant + } + items={statusIcon} + onStart={enableAnimation} + immediate={animationDisabled} + from={{ + transform: "scale3d(1.3, 1.3, 1)", + }} + enter={{ + opacity: 1, + transform: "scale3d(1, 1, 1)", + }} + leave={{ + position: "absolute", + opacity: 0, + }} + > + {(currentStatusIcon) => + currentStatusIcon && + ((animProps: any) => ( + + {currentStatusIcon} + + )) + } + +
+ + {illustration} +
+
+
+
+ ); +} + +StatusVisual.propTypes = { + status: PropTypes.oneOf([ + IndividualStepTypes.Waiting, + IndividualStepTypes.Prompting, + IndividualStepTypes.Working, + IndividualStepTypes.Success, + IndividualStepTypes.Error, + ]).isRequired, + color: PropTypes.string.isRequired, + number: PropTypes.number.isRequired, +}; + +/* eslint-disable react/prop-types */ +function StepIllustration({ number, status, withoutFirstStep }: any) { + const theme = useTheme(); + const renderIllustration = + status === IndividualStepTypes.Working || + status === IndividualStepTypes.Error || + status === IndividualStepTypes.Success || + withoutFirstStep; + + return ( +
+ {renderIllustration ? ( + + ) : ( +
+ {number} +
+ )} +
+ ); +} +/* eslint-enable react/prop-types */ + +export default StatusVisual; diff --git a/app/components/Stepper/Step/Step.tsx b/app/components/Stepper/Step/Step.tsx new file mode 100644 index 0000000..a998e50 --- /dev/null +++ b/app/components/Stepper/Step/Step.tsx @@ -0,0 +1,226 @@ +import { IndividualStepTypes } from "../stepper-statuses"; +import Divider from "./Divider"; +import StatusVisual from "./StatusVisual"; +import { TransactionBadge, textStyle, useTheme, GU } from "@1hive/1hive-ui"; +import { getNetwork } from "@/networks"; +import { springs } from "@/style/springs"; +import { useDisableAnimation } from "@hooks/useDisableAnimation"; +import { useWallet } from "@providers/Wallet"; +import PropTypes from "prop-types"; +import React, { useMemo } from "react"; +import { Transition, animated } from "react-spring/renderprops"; + +const AnimatedSpan = animated.span; + +function Step({ + title, + desc, + status, + number, + transactionHash, + showDivider, + withoutFirstStep, + ...props +}: any) { + const theme = useTheme(); + const { chainId } = useWallet(); + const network = getNetwork(chainId); + const [animationDisabled, enableAnimation] = useDisableAnimation(); + + const { visualColor, descColor } = useMemo(() => { + const appearance = { + [IndividualStepTypes.Waiting]: { + visualColor: theme.accent, + descColor: theme.contentSecondary, + }, + [IndividualStepTypes.Prompting]: { + visualColor: "#7CE0D6", + descColor: theme.contentSecondary, + }, + [IndividualStepTypes.Working]: { + visualColor: "#FFE862", + descColor: "#C3A22B", + }, + [IndividualStepTypes.Success]: { + visualColor: theme.positive, + descColor: theme.positive, + }, + [IndividualStepTypes.Error]: { + visualColor: theme.negative, + descColor: theme.negative, + }, + }; + + // @ts-expect-error + const { descColor, visualColor } = appearance[status]; + return { + visualColor: `${visualColor}`, + descColor: `${descColor}`, + }; + }, [status, theme]); + + return ( + <> +
+ +

+ {status === IndividualStepTypes.Error ? "Transaction failed" : title} +

+ +

+ + {(item) => + item && + ((transitionProps) => ( + + {item.currentDesc} + + )) + } + +

+ +
+ + {(currentHash) => (transitionProps) => + currentHash ? ( + + + + ) : null} + +
+ + {showDivider && ( + + )} +
+ + ); +} + +Step.propTypes = { + title: PropTypes.string, + desc: PropTypes.string, + transactionHash: PropTypes.string, + number: PropTypes.number, + status: PropTypes.oneOf([ + IndividualStepTypes.Waiting, + IndividualStepTypes.Prompting, + IndividualStepTypes.Working, + IndividualStepTypes.Success, + IndividualStepTypes.Error, + ]).isRequired, + showDivider: PropTypes.bool, +}; + +export default Step; diff --git a/app/components/Stepper/Stepper.tsx b/app/components/Stepper/Stepper.tsx new file mode 100644 index 0000000..a269cfc --- /dev/null +++ b/app/components/Stepper/Stepper.tsx @@ -0,0 +1,275 @@ +import React, { useCallback, useEffect, useReducer, useState } from "react"; +import PropTypes from "prop-types"; +import { Transition, animated } from "@react-spring/web"; +import { GU, Info, noop, springs, useTheme } from "@blossom-labs/rosette-ui"; +import { useMultiModal } from "../MultiModal/MultiModalProvider"; +import Step from "./Step/Step"; +import { TRANSACTION_SIGNING_DESC } from "./stepper-descriptions"; +import { IndividualStepTypes } from "./stepper-statuses"; +import useStepperLayout from "./useStepperLayout"; +import { useDisableAnimation } from "~/hooks/useDisableAnimation"; +import { useMounted } from "~/hooks/useMounted"; + +const AnimatedDiv = animated.div; + +const INITIAL_STATUS = IndividualStepTypes.Prompting; + +const DEFAULT_DESCRIPTIONS = TRANSACTION_SIGNING_DESC; + +function initialStepState(steps: any) { + return steps.map((_: any, i: any) => { + return { + status: i === 0 ? INITIAL_STATUS : IndividualStepTypes.Waiting, + hash: null, + }; + }); +} + +function reduceSteps( + steps: any, + [action, stepIndex, value]: [string, number, string] +) { + if (action === "setHash") { + steps[stepIndex].hash = value; + return [...steps]; + } + if (action === "setStatus") { + steps[stepIndex].status = value; + return [...steps]; + } + return steps; +} + +function Stepper({ steps, onComplete, onCompleteActions }: any) { + const theme = useTheme(); + const mounted = useMounted(); + const { close } = useMultiModal(); + const [animationDisabled, enableAnimation] = useDisableAnimation(); + const [stepperStage, setStepperStage] = useState(0); + const [stepState, updateStep] = useReducer( + reduceSteps, + initialStepState(steps) + ); + + const { outerBoundsRef, innerBoundsRef, layout } = useStepperLayout(); + + const stepsCount = steps.length - 1; + + const renderStep = useCallback( + (stepIndex, showDivider?) => { + const { title, descriptions: suppliedDescriptions } = steps[stepIndex]; + const { status, hash } = stepState[stepIndex]; + const descriptions = suppliedDescriptions || DEFAULT_DESCRIPTIONS; + + return ( +
  • + +
  • + ); + }, + [stepState, steps] + ); + + const renderSteps = useCallback(() => { + return steps.map((_: any, index: any) => { + const showDivider = + index < stepsCount && + stepState[index].status !== IndividualStepTypes.Waiting; + + return renderStep(index, showDivider); + }); + }, [renderStep, steps, stepsCount, stepState]); + + const updateStepStatus = useCallback( + (status) => { + if (mounted()) { + updateStep(["setStatus", stepperStage, status]); + } + }, + [stepperStage, mounted] + ); + + const updateHash = useCallback( + (hash) => { + if (mounted()) { + updateStep(["setHash", stepperStage, hash]); + } + }, + [stepperStage, mounted] + ); + + const handleSign = useCallback(() => { + const { handleSign } = steps[stepperStage]; + + updateStepStatus(INITIAL_STATUS); + + // Pass state updates as render props to handleSign + handleSign({ + setPrompting: () => updateStepStatus(IndividualStepTypes.Prompting), + setWorking: () => updateStepStatus(IndividualStepTypes.Working), + setError: () => { + updateStepStatus(IndividualStepTypes.Error); + }, + setSuccess: () => { + updateStepStatus(IndividualStepTypes.Success); + + // Advance to next step or fire complete callback + if (mounted()) { + if (stepperStage === stepsCount) { + onComplete(); + } else { + setStepperStage(stepperStage + 1); + } + } + }, + setHash: (hash: string) => updateHash(hash), + }); + }, [ + mounted, + onComplete, + steps, + stepperStage, + stepsCount, + updateStepStatus, + updateHash, + ]); + + useEffect(() => { + if (steps.length > 0) { + handleSign(); + } + }, [stepperStage]); + + const completed = + stepperStage === stepsCount && + stepState[stepperStage].status === IndividualStepTypes.Success; + + useEffect(() => { + let timeout: any; + if (completed && !onCompleteActions) { + timeout = setTimeout(() => close(), 2500); + } + return () => { + clearTimeout(timeout); + }; + }, [close, completed, onCompleteActions]); + + return ( +
    +
    +
      + {layout === "collapsed" && ( + <> + {steps.length > 1 && ( +

      + {stepperStage + 1} out of {steps.length} transactions +

      + )} + +
      + + {(currentStage: any) => (animProps: any) => + ( + + {renderStep(currentStage)} + + )} + +
      + + )} + {layout === "expanded" && renderSteps()} +
    +
    + {completed && ( +
    + + You might need to wait a few seconds for the UI to update + + {onCompleteActions && ( +
    {onCompleteActions}
    + )} +
    + )} +
    + ); +} + +Stepper.propTypes = { + steps: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + handleSign: PropTypes.func.isRequired, + descriptions: PropTypes.shape({ + [IndividualStepTypes.Waiting]: PropTypes.string, + [IndividualStepTypes.Prompting]: PropTypes.string, + [IndividualStepTypes.Working]: PropTypes.string, + [IndividualStepTypes.Success]: PropTypes.string, + [IndividualStepTypes.Error]: PropTypes.string, + }), + }) + ).isRequired, + onComplete: PropTypes.func, + onCompleteActions: PropTypes.node, +}; + +Stepper.defaultProps = { + onComplete: noop, +}; + +export default Stepper; diff --git a/app/components/Stepper/stepper-descriptions.ts b/app/components/Stepper/stepper-descriptions.ts new file mode 100644 index 0000000..271200f --- /dev/null +++ b/app/components/Stepper/stepper-descriptions.ts @@ -0,0 +1,19 @@ +import { IndividualStepTypes } from './stepper-statuses' + +export const TRANSACTION_SIGNING_DESC = { + [IndividualStepTypes.Waiting]: 'Waiting for signature', + [IndividualStepTypes.Prompting]: 'Waiting for signature', + [IndividualStepTypes.Working]: + 'Hang tight. Your transaction is being processed by the network…', + [IndividualStepTypes.Success]: + 'Your transaction has successfully been processed!', + [IndividualStepTypes.Error]: 'An error has occured', +} + +export const MESSAGE_SIGNING_DESC = { + [IndividualStepTypes.Waiting]: 'Waiting for signature', + [IndividualStepTypes.Prompting]: 'Waiting for signature', + [IndividualStepTypes.Working]: 'Message being signed', + [IndividualStepTypes.Success]: 'Message signed', + [IndividualStepTypes.Error]: 'An error has occured', +} diff --git a/app/components/Stepper/stepper-statuses.ts b/app/components/Stepper/stepper-statuses.ts new file mode 100644 index 0000000..4af36f0 --- /dev/null +++ b/app/components/Stepper/stepper-statuses.ts @@ -0,0 +1,13 @@ +export enum StepperStatusTypes { + Working = "STEPPER_WORKING", + Success = "STEPPER_SUCCESS", + Error = "STEPPER_ERROR", +} + +export enum IndividualStepTypes { + Prompting = "STEP_PROMPTING", + Waiting = "STEP_WAITING", + Working = "STEP_WORKING", + Success = "STEP_SUCCESS", + Error = "STEP_ERROR", +} diff --git a/app/components/Stepper/useStepperLayout.ts b/app/components/Stepper/useStepperLayout.ts new file mode 100644 index 0000000..9563fcd --- /dev/null +++ b/app/components/Stepper/useStepperLayout.ts @@ -0,0 +1,38 @@ +import { useEffect, useRef, useState, useLayoutEffect } from "react"; +import useMeasure from "react-use-measure"; +import { ResizeObserver } from "@juggle/resize-observer"; + +function useStepperLayout() { + const [outerBoundsRef, outerBounds] = useMeasure({ + polyfill: ResizeObserver, + }); + const innerBoundsRef = useRef(null); + const [innerBounds, setInnerBounds] = useState(null); + + // First render must always be in "expanded" mode so that our measurement reference point is accurate + const [layout, setLayout] = useState("expanded"); + + // It's important that we only query for the inner offsetWidth once so that our reference remains constant + useLayoutEffect(() => { + if (!innerBounds) { + // @ts-expect-error + setInnerBounds(innerBoundsRef.current.offsetWidth); + } + }, [innerBounds]); + + useEffect(() => { + const outerMeasured = outerBounds.width > 0; + + if (outerMeasured && outerBounds.width < innerBounds!) { + setLayout("collapsed"); + } + + if (outerMeasured && outerBounds.width >= innerBounds!) { + setLayout("expanded"); + } + }, [outerBounds, innerBounds]); + + return { outerBoundsRef, innerBoundsRef, layout }; +} + +export default useStepperLayout; diff --git a/app/hooks/useDisableAnimation.ts b/app/hooks/useDisableAnimation.ts new file mode 100644 index 0000000..00fc9d0 --- /dev/null +++ b/app/hooks/useDisableAnimation.ts @@ -0,0 +1,19 @@ +import { useCallback, useState } from "react"; + +type DisableAnimation = [ + animationDisabled: boolean, + enableAnimation: () => void +]; + +// Simple hook for performing Spring animations immediately +export function useDisableAnimation(): DisableAnimation { + const [animationDisabled, setAnimationDisabled] = useState(true); + + const enableAnimation = useCallback(() => { + if (animationDisabled) { + setAnimationDisabled(false); + } + }, [animationDisabled]); + + return [animationDisabled, enableAnimation]; +} diff --git a/app/hooks/useMounted.ts b/app/hooks/useMounted.ts new file mode 100644 index 0000000..a0fa15c --- /dev/null +++ b/app/hooks/useMounted.ts @@ -0,0 +1,16 @@ +import { useEffect, useRef, useCallback } from 'react' + +// Simple hook for checking a component is mounted prior to an async state update +export function useMounted() { + const mounted = useRef(true) + + const getMounted = useCallback(() => mounted.current, []) + + useEffect(() => { + return () => { + mounted.current = false + } + }, []) + + return getMounted +} diff --git a/package.json b/package.json index 4ded6c2..e492677 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@blossom-labs/rosette-react": "^0.1.1", "@blossom-labs/rosette-ui": "^0.0.1-alpha.6", + "@juggle/resize-observer": "^3.3.1", "@netlify/functions": "^1.0.0", "@react-spring/web": "^9.4.4", "@remix-run/netlify": "^1.4.0", @@ -29,6 +30,7 @@ "lodash.debounce": "^4.0.8", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-use-measure": "^2.1.1", "styled-components": "^5.3.3", "wagmi": "^0.2.26", "zustand": "^3.7.1" diff --git a/yarn.lock b/yarn.lock index 0ec3305..4a38fb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1190,6 +1190,11 @@ "@json-rpc-tools/types" "^1.7.6" "@pedrouid/environment" "^1.0.1" +"@juggle/resize-observer@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" + integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== + "@metamask/safe-event-emitter@2.0.0", "@metamask/safe-event-emitter@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" @@ -3243,6 +3248,11 @@ deasync@^0.1.0: bindings "^1.5.0" node-addon-api "^1.7.1" +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -7592,6 +7602,13 @@ react-spring@8.0.27: "@babel/runtime" "^7.3.1" prop-types "^15.5.8" +react-use-measure@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/react-use-measure/-/react-use-measure-2.1.1.tgz#5824537f4ee01c9469c45d5f7a8446177c6cc4ba" + integrity sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig== + dependencies: + debounce "^1.2.1" + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" From 25c0ce25df0c75d8d2e7d9f71dd9cf4199eb2f8b Mon Sep 17 00:00:00 2001 From: Gabi Date: Thu, 2 Jun 2022 20:50:06 -0300 Subject: [PATCH 3/4] chore: more changes --- app/components/MultiModal/MultiModalScreens.tsx | 10 ++-------- app/components/Stepper/Step/StatusVisual.tsx | 10 ++-------- app/components/Stepper/Step/Step.tsx | 17 +++++++++++------ app/springs.ts | 7 +++++++ 4 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 app/springs.ts diff --git a/app/components/MultiModal/MultiModalScreens.tsx b/app/components/MultiModal/MultiModalScreens.tsx index e25cfc5..26ef748 100644 --- a/app/components/MultiModal/MultiModalScreens.tsx +++ b/app/components/MultiModal/MultiModalScreens.tsx @@ -13,21 +13,15 @@ import { useLayout, useTheme, Viewport, - springs as baseSprings, } from "@blossom-labs/rosette-ui"; import { useInside } from "use-inside"; import { useDisableAnimation } from "~/hooks/useDisableAnimation"; +import { springs } from "~/springs"; import { MultiModalProvider, useMultiModal } from "./MultiModalProvider"; const DEFAULT_MODAL_WIDTH = 80 * GU; const AnimatedDiv = animated.div; -const springs = { - ...baseSprings, - gentle: { mass: 1, tension: 200, friction: 20 }, - tight: { mass: 0.6, tension: 500, friction: 40 }, -}; - type MultiModalScreensType = { screens: Array; }; @@ -228,7 +222,7 @@ const MultiModalContent = React.memo(function ModalContent({ > - //TransitionPhase LEAVE + // TransitionPhase LEAVE state === 3 ? springs.instant : springs.tight } items={step} diff --git a/app/components/Stepper/Step/StatusVisual.tsx b/app/components/Stepper/Step/StatusVisual.tsx index b239e1e..943d5d7 100644 --- a/app/components/Stepper/Step/StatusVisual.tsx +++ b/app/components/Stepper/Step/StatusVisual.tsx @@ -8,11 +8,11 @@ import { IconCross, IconCheck, useTheme, - springs as baseSprings, } from "@blossom-labs/rosette-ui"; +import { springs } from "~/springs"; +import { useDisableAnimation } from "~/hooks/useDisableAnimation"; import { IndividualStepTypes } from "../stepper-statuses"; import Illustration from "./Illustration"; -import { useDisableAnimation } from "~/hooks/useDisableAnimation"; const STATUS_ICONS = { [IndividualStepTypes.Error]: IconCross, @@ -21,12 +21,6 @@ const STATUS_ICONS = { const AnimatedDiv = animated.div; -const springs = { - ...baseSprings, - gentle: { mass: 1, tension: 200, friction: 20 }, - tight: { mass: 0.6, tension: 500, friction: 40 }, -}; - const spinAnimation = css` mask-image: linear-gradient(35deg, rgba(0, 0, 0, 0.1) 10%, rgba(0, 0, 0, 1)); animation: ${keyframes` diff --git a/app/components/Stepper/Step/Step.tsx b/app/components/Stepper/Step/Step.tsx index a998e50..1a79654 100644 --- a/app/components/Stepper/Step/Step.tsx +++ b/app/components/Stepper/Step/Step.tsx @@ -1,14 +1,19 @@ +import React, { useMemo } from "react"; +import PropTypes from "prop-types"; +import { Transition, animated } from "@react-spring/web"; +import { + TransactionBadge, + textStyle, + useTheme, + GU, +} from "@blossom-labs/rosette-ui"; import { IndividualStepTypes } from "../stepper-statuses"; import Divider from "./Divider"; import StatusVisual from "./StatusVisual"; -import { TransactionBadge, textStyle, useTheme, GU } from "@1hive/1hive-ui"; import { getNetwork } from "@/networks"; -import { springs } from "@/style/springs"; -import { useDisableAnimation } from "@hooks/useDisableAnimation"; +import { useDisableAnimation } from "~/hooks/useDisableAnimation"; +import { springs } from "~/springs"; import { useWallet } from "@providers/Wallet"; -import PropTypes from "prop-types"; -import React, { useMemo } from "react"; -import { Transition, animated } from "react-spring/renderprops"; const AnimatedSpan = animated.span; diff --git a/app/springs.ts b/app/springs.ts new file mode 100644 index 0000000..ad2ae3d --- /dev/null +++ b/app/springs.ts @@ -0,0 +1,7 @@ +import { springs as baseSprings } from "@blossom-labs/rosette-ui"; + +export const springs = { + ...baseSprings, + gentle: { mass: 1, tension: 200, friction: 20 }, + tight: { mass: 0.6, tension: 500, friction: 40 }, +}; From 130872d7f63b5a6f8d3e0a1ed0824a75139a59c0 Mon Sep 17 00:00:00 2001 From: 0xGabi Date: Fri, 3 Jun 2022 18:04:36 -0300 Subject: [PATCH 4/4] chore: final multimodal migration logic --- .../assets/loading-ring.svg | 0 .../ContractDescriptorScreen/index.tsx | 46 ++--- app/components/LoadingRing.tsx | 2 +- app/components/ModalFlows/ModalFlowBase.tsx | 21 +-- .../ModalFlows/SubmitEntriesScreens.tsx | 73 ++++++++ .../MultiModal/MultiModalScreens.tsx | 44 +++-- app/components/Stepper/Step/Illustration.tsx | 9 +- app/components/Stepper/Step/StatusVisual.tsx | 157 +++++++--------- app/components/Stepper/Step/Step.tsx | 172 +++++++++--------- app/components/Stepper/Stepper.tsx | 5 +- app/components/Stepper/useStepperLayout.ts | 6 +- 11 files changed, 293 insertions(+), 242 deletions(-) rename app/{components/AccountModule => }/assets/loading-ring.svg (100%) create mode 100644 app/components/ModalFlows/SubmitEntriesScreens.tsx diff --git a/app/components/AccountModule/assets/loading-ring.svg b/app/assets/loading-ring.svg similarity index 100% rename from app/components/AccountModule/assets/loading-ring.svg rename to app/assets/loading-ring.svg diff --git a/app/components/ContractDescriptorScreen/index.tsx b/app/components/ContractDescriptorScreen/index.tsx index 724029d..b4a53f3 100644 --- a/app/components/ContractDescriptorScreen/index.tsx +++ b/app/components/ContractDescriptorScreen/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useAccount } from "wagmi"; import { utils } from "ethers"; import { Button, GU, useViewport } from "@blossom-labs/rosette-ui"; @@ -22,6 +22,8 @@ import { HelperFunctionsPicker } from "./HelperFunctionsPicker"; import { FnDescriptorsCarousel } from "./FnDescriptorsCarousel"; import type { IPFSFnDescription } from "~/routes/fn-descriptions-upload"; import debounce from "lodash.debounce"; +import MultiModal from "../MultiModal/MultiModal"; +import SubmitEntriesScreens from "../ModalFlows/SubmitEntriesScreens"; const FN_DESCRIPTOR_HEIGHT = "527px"; @@ -54,6 +56,8 @@ export const ContractDescriptorScreen = ({ contractData: { abi, bytecode }, currentFnEntries, }: ContractDescriptorScreenProps) => { + const [modalVisible, setModalVisible] = useState(false); + const [modalMode, setModalMode] = useState(null); const { below } = useViewport(); const [{ data: accountData }] = useAccount(); const { fnSelected, fnDescriptorEntries, userFnDescriptions } = @@ -65,27 +69,18 @@ export const ContractDescriptorScreen = ({ const actionFetcher = useFetcher(); const { upsertEntries } = useRosetteActions(); - const handleSubmit = useCallback( - (event) => { - event.preventDefault(); - - const fnDescriptionsJSON = buildUploadDataJSON( - fnDescriptorEntries, - userFnDescriptions - ); - - actionFetcher.submit( - { - fnDescriptions: JSON.stringify(fnDescriptionsJSON), - }, - { - method: "post", - action: "/fn-descriptions-upload", - } - ); - }, - [actionFetcher, fnDescriptorEntries, userFnDescriptions] - ); + const handleHideModal = useCallback(() => { + setModalVisible(false); + }, []); + + const handleShowModal = useCallback((mode) => { + setModalVisible(true); + setModalMode(mode); + }, []); + + const handleSubmit = useCallback(() => { + handleShowModal("submit"); + }, [handleShowModal]); useEffect(() => { const submitEntries = async () => { @@ -176,6 +171,13 @@ export const ContractDescriptorScreen = ({ disabled={!accountData?.address || fnDescriptionsCounter === 0} /> + setModalMode(null)} + > + {modalMode === "Submit" && } + ); diff --git a/app/components/LoadingRing.tsx b/app/components/LoadingRing.tsx index 2599f66..d97b3da 100644 --- a/app/components/LoadingRing.tsx +++ b/app/components/LoadingRing.tsx @@ -1,6 +1,6 @@ import styled, { keyframes } from "styled-components"; -import loadingRing from "./assets/loading-ring.svg"; +import loadingRing from "~/assets/loading-ring.svg"; const spin = keyframes` from { diff --git a/app/components/ModalFlows/ModalFlowBase.tsx b/app/components/ModalFlows/ModalFlowBase.tsx index 9abd653..1280884 100644 --- a/app/components/ModalFlows/ModalFlowBase.tsx +++ b/app/components/ModalFlows/ModalFlowBase.tsx @@ -8,15 +8,6 @@ import Stepper from "../Stepper/Stepper"; // import { useActivity } from '@providers/ActivityProvider' -type TransactionType = { - data: any; - from: string | undefined; - to: string | undefined; - description?: string; - type?: string; - gasLimit?: number; -}; - const INDEX_NUMBER = ["First", "Second", "Third", "Fourth", "Fifth"]; type ModalFlowBaseType = { @@ -40,8 +31,18 @@ type TransactionStepsType = { title: string; handleSign: (params: HandleSignParamsType) => Promise; }; + type ArrayTransactionStepsType = Array | null; +type TransactionType = { + data: any; + from: string | undefined; + to: string | undefined; + description?: string; + type?: string; + gasLimit?: number; +}; + function ModalFlowBase({ loading, screens, @@ -97,8 +98,6 @@ function ModalFlowBase({ // TODO: Provide a better user experience than waiting on all transactions await tx.wait(); - // setAccountSetting("lastTxHash", account, chainId, tx.hash); - setSuccess(); } catch (err) { console.error(err); diff --git a/app/components/ModalFlows/SubmitEntriesScreens.tsx b/app/components/ModalFlows/SubmitEntriesScreens.tsx new file mode 100644 index 0000000..4ee9142 --- /dev/null +++ b/app/components/ModalFlows/SubmitEntriesScreens.tsx @@ -0,0 +1,73 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import ModalFlowBase from "./ModalFlowBase"; +import { useMultiModal } from "../MultiModal/MultiModalProvider"; + +function SubmitEntriesScreens() { + const [transactions, setTransactions] = useState([]); + + const getTransactions = useCallback( + async (onComplete) => { + await priceOracleActions.updatePriceOracle((intent) => { + setTransactions(intent); + onComplete(); + }); + }, + [priceOracleActions] + ); + + const screens = useMemo( + () => [ + { + title: "Update Price Oracle", + graphicHeader: true, + content: , + }, + ], + [getTransactions] + ); + + return ( + + ); +} + +const PriceOracle = React.memo(function ExecuteProposal({ getTransactions }) { + const { next } = useMultiModal(); + + useEffect(() => { + getTransactions(() => { + next(); + }); + }, [getTransactions, next]); + + return
    ; +}); + +export default SubmitEntriesScreens; + +// const handleSubmit = useCallback( +// (event) => { +// event.preventDefault(); + +// const fnDescriptionsJSON = buildUploadDataJSON( +// fnDescriptorEntries, +// userFnDescriptions +// ); + +// actionFetcher.submit( +// { +// fnDescriptions: JSON.stringify(fnDescriptionsJSON), +// }, +// { +// method: "post", +// action: "/fn-descriptions-upload", +// } +// ); +// }, +// [actionFetcher, fnDescriptorEntries, userFnDescriptions] +// ); diff --git a/app/components/MultiModal/MultiModalScreens.tsx b/app/components/MultiModal/MultiModalScreens.tsx index 26ef748..27264e6 100644 --- a/app/components/MultiModal/MultiModalScreens.tsx +++ b/app/components/MultiModal/MultiModalScreens.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/display-name */ import React, { useCallback, useState } from "react"; import PropTypes from "prop-types"; import styled from "styled-components"; @@ -94,34 +95,17 @@ function MultiModalFrame({ visible, onClosed }: MultiModalFrameType) { > {({ width }) => { return ( - div > div > div { - border-radius: ${2 * RADIUS}px !important; - } - `} >
    {!disableClose && ( - + - + )}
    -
    + ); }} @@ -150,7 +134,6 @@ const MultiModalContent = React.memo(function ModalContent({ }: { viewportWidth: number; }) { - const theme = useTheme(); const { step, direction, getScreen } = useMultiModal(); const [applyStaticHeight, setApplyStaticHeight] = useState(false); const [height, setHeight] = useState(0); @@ -204,7 +187,7 @@ const MultiModalContent = React.memo(function ModalContent({ ); }, - [smallMode, theme, viewportWidth] + [smallMode, viewportWidth] ); return ( @@ -282,6 +265,21 @@ const MultiModalContent = React.memo(function ModalContent({ export default MultiModalScreens; +/* TODO: Add radius option to Modal in @rosette/ui */ +const StyledModal = styled(Modal)` + z-index: 2; + & > div > div > div { + border-radius: ${2 * RADIUS}px !important; + } +`; + +const StyledButtonIcon = styled(ButtonIcon)` + position: absolute; + top: ${2.5 * GU}px; + right: ${2.5 * GU}px; + z-index: 2; +`; + const HeaderContainer = styled.div<{ standardPadding: number; smallMode: boolean; diff --git a/app/components/Stepper/Step/Illustration.tsx b/app/components/Stepper/Step/Illustration.tsx index 7308e32..b369c07 100644 --- a/app/components/Stepper/Step/Illustration.tsx +++ b/app/components/Stepper/Step/Illustration.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/alt-text */ import React from "react"; import styled from "styled-components"; import { IndividualStepTypes } from "../stepper-statuses"; @@ -12,13 +13,7 @@ const illustrations = { [IndividualStepTypes.Error]: signRequestFailIllustration, }; -function Illustration({ - status, - index, -}: { - status: IndividualStepTypes; - index: number; -}) { +function Illustration({ status, index }: { status: string; index: number }) { return ( <> {status === IndividualStepTypes.Prompting ? ( diff --git a/app/components/Stepper/Step/StatusVisual.tsx b/app/components/Stepper/Step/StatusVisual.tsx index 943d5d7..a0b0231 100644 --- a/app/components/Stepper/Step/StatusVisual.tsx +++ b/app/components/Stepper/Step/StatusVisual.tsx @@ -1,14 +1,8 @@ import React, { useMemo } from "react"; import PropTypes from "prop-types"; import { Transition, animated } from "@react-spring/web"; -import { css, keyframes } from "styled-components"; -import { - GU, - textStyle, - IconCross, - IconCheck, - useTheme, -} from "@blossom-labs/rosette-ui"; +import styled, { css, keyframes } from "styled-components"; +import { GU, textStyle, IconCross, IconCheck } from "@blossom-labs/rosette-ui"; import { springs } from "~/springs"; import { useDisableAnimation } from "~/hooks/useDisableAnimation"; import { IndividualStepTypes } from "../stepper-statuses"; @@ -53,7 +47,6 @@ function StatusVisual({ withoutFirstStep, ...props }: any) { - const theme = useTheme(); const [animationDisabled, enableAnimation] = useDisableAnimation(); const [statusIcon, illustration] = useMemo(() => { @@ -62,6 +55,7 @@ function StatusVisual({ return [ Icon && , + // eslint-disable-next-line react/jsx-key
    -
    +
    @@ -124,23 +109,9 @@ function StatusVisual({ {(currentStatusIcon) => currentStatusIcon && ((animProps: any) => ( - + {currentStatusIcon} - + )) } @@ -148,24 +119,7 @@ function StatusVisual({ {illustration}
    -
    +
    ); @@ -185,7 +139,6 @@ StatusVisual.propTypes = { /* eslint-disable react/prop-types */ function StepIllustration({ number, status, withoutFirstStep }: any) { - const theme = useTheme(); const renderIllustration = status === IndividualStepTypes.Working || status === IndividualStepTypes.Error || @@ -193,38 +146,68 @@ function StepIllustration({ number, status, withoutFirstStep }: any) { withoutFirstStep; return ( -
    + {renderIllustration ? ( ) : ( -
    - {number} -
    + {number} )} -
    + ); } /* eslint-enable react/prop-types */ export default StatusVisual; + +const StyledAnimatedDiv = styled(AnimatedDiv)<{ color: string }>` + display: flex; + justify-content: center; + align-items: center; + border-radius: 100%; + padding: ${0.25 * GU}px; + background-color: ${({ theme }) => theme.surface}; + color: ${({ color }) => color}; + border: 1px solid currentColor; + bottom: 0; + right: 0; +`; + +const StyledDiv = styled.div<{ status: string; color: string }>` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + border-radius: 100%; + ${({ theme, status, color }) => `border: 2px solid + ${status === IndividualStepTypes.Waiting ? "transparent" : color}; + ${status === IndividualStepTypes.Prompting ? pulseAnimation : ""} + ${status === IndividualStepTypes.Working ? spinAnimation : ""} + ${ + status === IndividualStepTypes.Prompting + ? `background-color: ${theme.contentSecondary};` + : "" + }`} +`; + +const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: ${12 * GU}px; + height: ${12 * GU}px; +`; + +const NumberContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + background-color: ${({ theme }) => theme.contentSecondary}; + height: 100%; + width: 100%; + border-radius: 100%; + color: ${({ theme }) => theme.positiveContent}; + ${textStyle("title1")}; + font-weight: 600; +`; diff --git a/app/components/Stepper/Step/Step.tsx b/app/components/Stepper/Step/Step.tsx index 1a79654..a1fe23f 100644 --- a/app/components/Stepper/Step/Step.tsx +++ b/app/components/Stepper/Step/Step.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/display-name */ import React, { useMemo } from "react"; import PropTypes from "prop-types"; import { Transition, animated } from "@react-spring/web"; @@ -7,13 +8,13 @@ import { useTheme, GU, } from "@blossom-labs/rosette-ui"; +import { useNetwork } from "wagmi"; import { IndividualStepTypes } from "../stepper-statuses"; import Divider from "./Divider"; import StatusVisual from "./StatusVisual"; -import { getNetwork } from "@/networks"; import { useDisableAnimation } from "~/hooks/useDisableAnimation"; import { springs } from "~/springs"; -import { useWallet } from "@providers/Wallet"; +import styled from "styled-components"; const AnimatedSpan = animated.span; @@ -28,8 +29,8 @@ function Step({ ...props }: any) { const theme = useTheme(); - const { chainId } = useWallet(); - const network = getNetwork(chainId); + const [{ data: networkData }] = useNetwork(); + const { chain } = networkData || {}; const [animationDisabled, enableAnimation] = useDisableAnimation(); const { visualColor, descColor } = useMemo(() => { @@ -66,47 +67,18 @@ function Step({ return ( <> -
    - + -

    + {status === IndividualStepTypes.Error ? "Transaction failed" : title} -

    - -

    + + + {(item) => item && - ((transitionProps) => ( - ( + {item.currentDesc} - + )) } -

    - -
    + + + - {(currentHash) => (transitionProps) => + {(currentHash) => (transitionProps: any) => currentHash ? ( - + - + ) : null} -
    - - {showDivider && ( - - )} -
    + + + {showDivider && } + ); } @@ -229,3 +171,61 @@ Step.propTypes = { }; export default Step; + +const Container = styled.div` + position: relative; + display: flex; + flex-direction: column; + align-items: center; + + width: ${31 * GU}px; +`; + +const StatusVisualStyled = styled(StatusVisual)` + margin-bottom: ${3 * GU}px; +`; + +const StyledH2 = styled.h2` + ${textStyle("title4")} + height:${6 * GU}px; + line-height: 1.2; + text-align: center; + margin-bottom: ${1 * GU}px; +`; + +const StyledP = styled.p` + width: 100%; + position: relative; + text-align: center; + color: ${({ theme }) => theme.contentSecondary}; + line-height: 1.2; +`; + +const AnimatedSpanStyled = styled(AnimatedSpan)<{ color: string }>` + display: flex; + justify-content: center; + left: 0; + top: 0; + width: 100%; + color: ${({ color }) => color}; +`; + +const TransitionContainer = styled.div` + margin-top: ${1.5 * GU}px; + position: relative; + width: 100%; +`; + +const TransitionAnimatedSpanStyled = styled(AnimatedSpan)` + display: flex; + justify-content: center; + width: 100%; +`; + +const DividerStyled = styled(Divider)` + position: absolute; + top: ${6 * GU}px; + right: 0; + + transform: translateX(50%); +`; diff --git a/app/components/Stepper/Stepper.tsx b/app/components/Stepper/Stepper.tsx index a269cfc..a843961 100644 --- a/app/components/Stepper/Stepper.tsx +++ b/app/components/Stepper/Stepper.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/display-name */ import React, { useCallback, useEffect, useReducer, useState } from "react"; import PropTypes from "prop-types"; import { Transition, animated } from "@react-spring/web"; @@ -146,7 +147,7 @@ function Stepper({ steps, onComplete, onCompleteActions }: any) { if (steps.length > 0) { handleSign(); } - }, [stepperStage]); + }, [handleSign, stepperStage, steps.length]); const completed = stepperStage === stepsCount && @@ -239,7 +240,7 @@ function Stepper({ steps, onComplete, onCompleteActions }: any) { margin-top: ${5 * GU}px; `} > - You might need to wait a few seconds for the UI to update + The UI may take a few seconds to update {onCompleteActions && (
    {onCompleteActions}
    diff --git a/app/components/Stepper/useStepperLayout.ts b/app/components/Stepper/useStepperLayout.ts index 9563fcd..854f8c2 100644 --- a/app/components/Stepper/useStepperLayout.ts +++ b/app/components/Stepper/useStepperLayout.ts @@ -16,18 +16,18 @@ function useStepperLayout() { useLayoutEffect(() => { if (!innerBounds) { // @ts-expect-error - setInnerBounds(innerBoundsRef.current.offsetWidth); + setInnerBounds(innerBoundsRef?.current?.offsetWidth); } }, [innerBounds]); useEffect(() => { const outerMeasured = outerBounds.width > 0; - if (outerMeasured && outerBounds.width < innerBounds!) { + if (outerMeasured && innerBounds && outerBounds.width < innerBounds) { setLayout("collapsed"); } - if (outerMeasured && outerBounds.width >= innerBounds!) { + if (outerMeasured && innerBounds && outerBounds.width >= innerBounds) { setLayout("expanded"); } }, [outerBounds, innerBounds]);