Skip to content

Commit

Permalink
Feature: Share safe app (safe-global#519)
Browse files Browse the repository at this point in the history
* add share button to the compact app card, landing preparations

* landing wip

* landing page wip

* landing page wip

* move the details to a separate component

* CTA alignment on the share page

* wip

* working demo flow

* wip stuff

* add redirect in the open safe flow

* safe selector wip

* safe selector wip

* adjust the CTA for variable height content

* fix the test for safe creation watcher

* fix deps

* add analytics for clicking demo safe

* add apps event after safe creation

* rename useapp component, remove fallback in chain indi

* extract 1 to a constant

* allow passing initial state when rendering the test setup

* add real fetch to test environment, tests wip

* mock usewallet

* Safe selector test

* increase test timeouts

* use copybutton for share button

* pass chain shortname to pre-select network

* remove undefined check, add client_url for apps request

* replace chainId query param with chain

* test fix

* fix share button width

* parallelize tests

* hide sidebar on app landing page

* switch useLastSafe to default export, use classnames in pagelayout and strict comparison

* show spinner when loading chains or router isn't ready

* remove ariaLabel prop and use the tooltip text

* render whitespace in chainindicator

* add newline
  • Loading branch information
mmv08 authored Sep 16, 2022
1 parent 20cebb9 commit 489ea76
Show file tree
Hide file tree
Showing 38 changed files with 1,528 additions and 88 deletions.
1 change: 1 addition & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// Used for __tests__/testing-library.js
// Learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect'
import 'whatwg-fetch'

jest.mock('@web3-onboard/coinbase', () => jest.fn())
jest.mock('@web3-onboard/fortmatic', () => jest.fn())
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"prettier": "prettier -w \"{src,cypress,mocks,scripts}/**/*.{ts,tsx,css,js}\"",
"fix": "yarn lint:fix && ts-prune && yarn prettier",
"test": "cross-env TZ=CET DEBUG_PRINT_LIMIT=30000 jest",
"test:ci": "yarn test --ci --coverage --json --watchAll=false --testLocationInResults --runInBand --outputFile=jest.results.json",
"test:ci": "yarn test --ci --coverage --json --watchAll=false --testLocationInResults --outputFile=jest.results.json",
"cmp": "./scripts/cmp.sh",
"routes": "node scripts/generate-routes.js > src/config/routes.ts && prettier -w src/config/routes.ts && cat src/config/routes.ts",
"css-vars": "ts-node-esm ./scripts/css-vars.ts > ./src/styles/vars.css && prettier -w src/styles/vars.css",
Expand Down Expand Up @@ -44,7 +44,7 @@
"@gnosis.pm/safe-deployments": "^1.15.0",
"@gnosis.pm/safe-ethers-lib": "^1.5.0",
"@gnosis.pm/safe-modules-deployments": "^1.0.0",
"@gnosis.pm/safe-react-gateway-sdk": "^3.3.5",
"@gnosis.pm/safe-react-gateway-sdk": "^3.4.0",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.9.3",
"@mui/x-date-pickers": "^5.0.0-beta.6",
Expand All @@ -66,6 +66,7 @@
"ethereum-blockies-base64": "^1.0.2",
"ethers": "^5.6.8",
"exponential-backoff": "^3.1.0",
"fuse.js": "^6.6.2",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"next": "12.2.0",
Expand Down Expand Up @@ -108,14 +109,14 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"fuse.js": "^6.6.2",
"jest": "^28.1.2",
"jest-environment-jsdom": "^28.1.2",
"pre-commit": "^1.2.2",
"prettier": "^2.7.0",
"ts-node": "^10.8.2",
"ts-prune": "^0.10.3",
"typechain": "^8.0.0",
"typescript": "4.7.4"
"typescript": "4.7.4",
"whatwg-fetch": "3.6.2"
}
}
12 changes: 12 additions & 0 deletions public/images/apps-demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion src/components/common/ChainIndicator/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ type ChainIndicatorProps = {
chainId?: string
inline?: boolean
className?: string
renderWhiteSpaceIfNoChain?: boolean
}

const ChainIndicator = ({ chainId, className, inline = false }: ChainIndicatorProps): ReactElement => {
const ChainIndicator = ({
chainId,
className,
inline = false,
renderWhiteSpaceIfNoChain = true,
}: ChainIndicatorProps): ReactElement | null => {
const currentChainId = useChainId()
const id = chainId || currentChainId
const chainConfig = useAppSelector((state) => selectChainById(state, id))
Expand All @@ -26,6 +32,8 @@ const ChainIndicator = ({ chainId, className, inline = false }: ChainIndicatorPr
}
}, [chainConfig])

if (!chainConfig?.chainName && !renderWhiteSpaceIfNoChain) return null

return (
<span style={style} className={classnames(inline ? css.inlineIndicator : css.indicator, className)}>
{chainConfig?.chainName || ' '}
Expand Down
11 changes: 7 additions & 4 deletions src/components/common/CopyButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ const CopyButton = ({
text,
className,
children,
initialToolTipText = 'Copy to clipboard',
}: {
text: string
className?: string
children?: ReactNode
initialToolTipText?: string
ariaLabel?: string
}): ReactElement => {
const [tooltipText, setTooltipText] = useState('Copy to clipboard')
const [tooltipText, setTooltipText] = useState(initialToolTipText)

const handleCopy = useCallback(
(e: SyntheticEvent) => {
Expand All @@ -23,12 +26,12 @@ const CopyButton = ({
)

const handleMouseLeave = useCallback(() => {
setTimeout(() => setTooltipText('Copy to clipboard'), 500)
}, [])
setTimeout(() => setTooltipText(initialToolTipText), 500)
}, [initialToolTipText])

return (
<Tooltip title={tooltipText} placement="top" onMouseLeave={handleMouseLeave}>
<IconButton onClick={handleCopy} size="small" className={className}>
<IconButton aria-label={initialToolTipText} onClick={handleCopy} size="small" className={className}>
{children ?? <ContentCopyIcon fontSize="small" color="border" sx={{ width: '16px', height: '16px' }} />}
</IconButton>
</Tooltip>
Expand Down
7 changes: 5 additions & 2 deletions src/components/common/PageLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState, type ReactElement } from 'react'
import cn from 'classnames'
import { Drawer } from '@mui/material'
import { useRouter } from 'next/router'

Expand All @@ -7,10 +8,12 @@ import Header from '@/components/common//Header'
import css from './styles.module.css'
import SafeLoadingError from '../SafeLoadingError'
import Footer from '../Footer'
import { AppRoutes } from '@/config/routes'

const PageLayout = ({ children }: { children: ReactElement }): ReactElement => {
const router = useRouter()
const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState<boolean>(false)
const hideSidebar = router.pathname === AppRoutes.share.safeApp

const onMenuToggle = (): void => {
setIsMobileDrawerOpen((prev) => !prev)
Expand All @@ -29,14 +32,14 @@ const PageLayout = ({ children }: { children: ReactElement }): ReactElement => {
</header>

{/* Desktop sidebar */}
<aside className={css.sidebar}>{sidebar}</aside>
{!hideSidebar && <aside className={css.sidebar}>{sidebar}</aside>}

{/* Mobile sidebar */}
<Drawer variant="temporary" anchor="left" open={isMobileDrawerOpen} onClose={onMenuToggle}>
{sidebar}
</Drawer>

<div className={css.main}>
<div className={cn(css.main, hideSidebar && css.mainNoSidebar)}>
<div className={css.content}>
<SafeLoadingError>{children}</SafeLoadingError>
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/components/common/PageLayout/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
flex-direction: column;
}

.mainNoSidebar {
padding-left: 0;
}

.content {
flex: 1;
position: relative;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { renderHook } from '@/tests/test-utils'
import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation'
import * as router from 'next/router'
import { NextRouter } from 'next/router'
import * as web3 from '@/hooks/wallets/web3'
import * as pendingSafe from '@/components/create-safe/status/usePendingSafeCreation'
import * as chainIdModule from '@/hooks/useChainId'
import { Web3Provider } from '@ethersproject/providers'
import { PendingSafeData } from '@/components/create-safe'
import useWatchSafeCreation from '@/components/create-safe/status/hooks/useWatchSafeCreation'
import { AppRoutes } from '@/config/routes'
import { NextRouter } from 'next/router'
import { CONFIG_SERVICE_CHAINS } from '@/tests/mocks'

describe('useWatchSafeCreation', () => {
beforeEach(() => {
Expand All @@ -32,6 +33,7 @@ describe('useWatchSafeCreation', () => {
pendingSafe: { txHash: '0x10' } as PendingSafeData,
setPendingSafe: setPendingSafeSpy,
setStatus: setStatusSpy,
chainId: '4',
}),
)

Expand All @@ -52,6 +54,7 @@ describe('useWatchSafeCreation', () => {
pendingSafe: {} as PendingSafeData,
setPendingSafe: setPendingSafeSpy,
setStatus: setStatusSpy,
chainId: '4',
}),
)

Expand All @@ -70,6 +73,7 @@ describe('useWatchSafeCreation', () => {
pendingSafe: {} as PendingSafeData,
setPendingSafe: setPendingSafeSpy,
setStatus: setStatusSpy,
chainId: '4',
}),
)

Expand All @@ -89,6 +93,7 @@ describe('useWatchSafeCreation', () => {
pendingSafe: {} as PendingSafeData,
setPendingSafe: setPendingSafeSpy,
setStatus: setStatusSpy,
chainId: '4',
}),
)

Expand All @@ -108,14 +113,15 @@ describe('useWatchSafeCreation', () => {
pendingSafe: undefined,
setPendingSafe: setPendingSafeSpy,
setStatus: setStatusSpy,
chainId: '4',
}),
)

expect(pollSafeInfoSpy).not.toHaveBeenCalled()
expect(setPendingSafeSpy).toHaveBeenCalledWith(undefined)
})

it('should navigate to the dashboard on INDEXED', () => {
it('should navigate to the dashboard on INDEXED', async () => {
jest.spyOn(chainIdModule, 'default').mockReturnValue('4')
const pushMock = jest.fn()
jest.spyOn(router, 'useRouter').mockReturnValue({
Expand All @@ -128,16 +134,27 @@ describe('useWatchSafeCreation', () => {
const setStatusSpy = jest.fn()
const setPendingSafeSpy = jest.fn()

renderHook(() =>
useWatchSafeCreation({
status: SafeCreationStatus.INDEXED,
safeAddress: '0x10',
pendingSafe: {} as PendingSafeData,
setPendingSafe: setPendingSafeSpy,
setStatus: setStatusSpy,
}),
renderHook(
() =>
useWatchSafeCreation({
status: SafeCreationStatus.INDEXED,
safeAddress: '0x10',
pendingSafe: {} as PendingSafeData,
setPendingSafe: setPendingSafeSpy,
setStatus: setStatusSpy,
chainId: '4',
}),
{
initialReduxState: {
chains: {
data: CONFIG_SERVICE_CHAINS,
error: undefined,
loading: false,
},
},
},
)

expect(pushMock).toHaveBeenCalledWith({ pathname: AppRoutes.safe.home, query: { safe: '0x10' } })
expect(pushMock).toHaveBeenCalledWith({ pathname: AppRoutes.safe.home, query: { safe: 'rin:0x10' } })
})
})
31 changes: 26 additions & 5 deletions src/components/create-safe/status/hooks/useWatchSafeCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@ import { useRouter } from 'next/router'
import { pollSafeInfo } from '@/components/create-safe/status/usePendingSafeCreation'
import { AppRoutes } from '@/config/routes'
import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation'
import useChainId from '@/hooks/useChainId'
import { trackEvent, CREATE_SAFE_EVENTS } from '@/services/analytics'
import { trackEvent, CREATE_SAFE_EVENTS, SAFE_APPS_EVENTS } from '@/services/analytics'
import { useAppSelector } from '@/store'
import { selectChainById } from '@/store/chainsSlice'

const useWatchSafeCreation = ({
status,
safeAddress,
pendingSafe,
setPendingSafe,
setStatus,
chainId,
}: {
status: SafeCreationStatus
safeAddress: string | undefined
pendingSafe: PendingSafeData | undefined
setPendingSafe: Dispatch<SetStateAction<PendingSafeData | undefined>>
setStatus: Dispatch<SetStateAction<SafeCreationStatus>>
chainId: string
}) => {
const router = useRouter()
const chainId = useChainId()
const chain = useAppSelector((state) => selectChainById(state, chainId))

useEffect(() => {
const checkCreatedSafe = async (chainId: string, address: string) => {
Expand All @@ -35,8 +38,26 @@ const useWatchSafeCreation = ({

if (status === SafeCreationStatus.INDEXED) {
trackEvent(CREATE_SAFE_EVENTS.GET_STARTED)
const chainPrefix = chain?.shortName

safeAddress && router.push({ pathname: AppRoutes.safe.home, query: { safe: safeAddress } })
if (safeAddress && chainPrefix) {
const address = `${chainPrefix}:${safeAddress}`
const redirectUrl = router.query?.safeViewRedirectURL
if (typeof redirectUrl === 'string') {
// We're prepending the safe address directly here because the `router.push` doesn't parse
// The URL for already existing query params
const hasQueryParams = redirectUrl.includes('?')
const appendChar = hasQueryParams ? '&' : '?'

if (redirectUrl.includes('apps')) {
trackEvent({ ...SAFE_APPS_EVENTS.SHARED_APP_OPEN_AFTER_SAFE_CREATION })
}

router.push(redirectUrl + `${appendChar}safe=${address}`)
} else {
router.push({ pathname: AppRoutes.safe.home, query: { safe: address } })
}
}
}

if (status === SafeCreationStatus.SUCCESS) {
Expand All @@ -51,7 +72,7 @@ const useWatchSafeCreation = ({
setPendingSafe((prev) => (prev ? { ...prev, txHash: undefined } : undefined))
}
}
}, [router, safeAddress, setPendingSafe, status, pendingSafe, setStatus, chainId])
}, [router, safeAddress, setPendingSafe, status, pendingSafe, setStatus, chainId, chain])
}

export default useWatchSafeCreation
2 changes: 1 addition & 1 deletion src/components/create-safe/status/useSafeCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export const useSafeCreation = () => {
}, [chainId, dispatch, isCreationPending, pendingSafe, provider, safeCreationCallback])

usePendingSafeCreation({ txHash: pendingSafe?.txHash, setStatus })
useWatchSafeCreation({ status, safeAddress, pendingSafe, setPendingSafe, setStatus })
useWatchSafeCreation({ status, safeAddress, pendingSafe, setPendingSafe, setStatus, chainId })

useEffect(() => {
if (
Expand Down
Loading

0 comments on commit 489ea76

Please sign in to comment.