Skip to content

Commit

Permalink
Feat: add swaps card to safe apps list (#3786)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmealy authored Jun 11, 2024
1 parent 2d7b400 commit 9720343
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 33 deletions.
4 changes: 2 additions & 2 deletions src/components/dashboard/ActivityRewardsSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const ActivityRewardsSection = () => {
}

return (
<Grid item xs={12}>
<>
<Card className={css.widgetWrapper}>
<SvgIcon
component={Asterix}
Expand Down Expand Up @@ -103,7 +103,7 @@ const ActivityRewardsSection = () => {
</Grid>
</Grid>
</Card>
</Grid>
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@

.links {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
align-items: center;
margin-top: var(--space-3);
text-wrap: nowrap;
Expand Down
16 changes: 10 additions & 6 deletions src/components/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import ActivityRewardsSection from '@/components/dashboard/ActivityRewardsSectio
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
import css from './styles.module.css'
import SwapWidget from '@/features/swap/components/SwapWidget'

const RecoveryHeader = dynamic(() => import('@/features/recovery/components/RecoveryHeader'))
const SwapWidget = dynamic(() => import('@/features/swap/components/SwapWidget'))

const Dashboard = (): ReactElement => {
const router = useRouter()
Expand All @@ -44,13 +44,17 @@ const Dashboard = (): ReactElement => {
<FirstSteps />
</Grid>

<Grid item xs={12} className={css.hideIfEmpty}>
<SwapWidget />
</Grid>

{safe.deployed && (
<>
<ActivityRewardsSection />
<Grid item xs={12} xl={6} className={css.hideIfEmpty}>
<SwapWidget />
</Grid>

<Grid item xs className={css.hideIfEmpty}>
<ActivityRewardsSection />
</Grid>

<Grid item xs={12} />

<Grid item xs={12} lg={6}>
<AssetsWidget />
Expand Down
31 changes: 31 additions & 0 deletions src/components/safe-apps/NativeSwapsCard/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Meta, StoryObj } from '@storybook/react'
import NativeSwapsCard from './index'
import { Box } from '@mui/material'
import { StoreDecorator } from '@/stories/storeDecorator'

const meta = {
component: NativeSwapsCard,
parameters: {
componentSubtitle: 'Renders a promo card for native swaps',
},

decorators: [
(Story) => {
return (
<StoreDecorator initialState={{ chains: { data: [{ chainId: '11155111', features: ['NATIVE_SWAPS'] }] } }}>
<Box sx={{ maxWidth: '500px' }}>
<Story />
</Box>
</StoreDecorator>
)
},
],
tags: ['autodocs'],
} satisfies Meta<typeof NativeSwapsCard>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {},
}
61 changes: 61 additions & 0 deletions src/components/safe-apps/NativeSwapsCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import CardHeader from '@mui/material/CardHeader'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import { Button, Paper, Stack } from '@mui/material'
import SafeAppIconCard from '../SafeAppIconCard'
import css from './styles.module.css'
import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps'
import Track from '@/components/common/Track'
import Link from 'next/link'
import { AppRoutes } from '@/config/routes'
import { useRouter } from 'next/router'
import useLocalStorage from '@/services/local-storage/useLocalStorage'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'

const SWAPS_APP_CARD_STORAGE_KEY = 'showSwapsAppCard'

const NativeSwapsCard = () => {
const router = useRouter()
const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS)
const [isSwapsCardVisible = true, setIsSwapsCardVisible] = useLocalStorage<boolean>(SWAPS_APP_CARD_STORAGE_KEY)
if (!isSwapFeatureEnabled || !isSwapsCardVisible) return null

return (
<Paper className={css.container}>
<CardHeader
className={css.header}
avatar={
<div className={css.iconContainer}>
<SafeAppIconCard src="/images/common/swap.svg" alt="Swap Icon" width={24} height={24} />
</div>
}
/>

<CardContent className={css.content}>
<Typography className={css.title} variant="h5">
Native swaps are here!
</Typography>

<Typography className={css.description} variant="body2" color="text.secondary">
Experience seamless trading with better decoding and security in native swaps.
</Typography>

<Stack direction="row" gap={2} className={css.buttons}>
<Track {...SWAP_EVENTS.OPEN_SWAPS} label={SWAP_LABELS.safeAppsPromoWidget}>
<Link href={{ pathname: AppRoutes.swap, query: { safe: router.query.safe } }} passHref legacyBehavior>
<Button variant="contained" size="small">
Try now
</Button>
</Link>
</Track>
<Button onClick={() => setIsSwapsCardVisible(false)} size="small" variant="text" sx={{ px: '16px' }}>
Don&apos;t show
</Button>
</Stack>
</CardContent>
</Paper>
)
}

export default NativeSwapsCard
50 changes: 50 additions & 0 deletions src/components/safe-apps/NativeSwapsCard/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.container {
transition: background-color 0.3s ease-in-out, border 0.3s ease-in-out;
border: 1px solid transparent;
height: 100%;
}

.container:hover {
background-color: var(--color-background-light);
border: 1px solid var(--color-secondary-light);
}

.header {
padding: var(--space-3) var(--space-2) var(--space-1) var(--space-2);
}

.content {
padding: var(--space-2);
}

.iconContainer {
position: relative;
background: var(--color-secondary-light);
border-radius: 50%;
display: flex;
padding: var(--space-1);
}

.title {
line-height: 175%;
margin: 0;

flex-grow: 1;

white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.description {
/* Truncate Safe App Description (3 lines) */
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

.buttons {
padding-top: var(--space-2);
white-space: nowrap;
}
5 changes: 5 additions & 0 deletions src/components/safe-apps/SafeAppList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import useSafeAppPreviewDrawer from '@/hooks/safe-apps/useSafeAppPreviewDrawer'
import css from './styles.module.css'
import { Skeleton } from '@mui/material'
import { useOpenedSafeApps } from '@/hooks/safe-apps/useOpenedSafeApps'
import NativeSwapsCard from '@/components/safe-apps/NativeSwapsCard'

type SafeAppListProps = {
safeAppsList: SafeAppData[]
Expand All @@ -20,6 +21,7 @@ type SafeAppListProps = {
removeCustomApp?: (safeApp: SafeAppData) => void
title: string
query?: string
isFiltered?: boolean
}

const SafeAppList = ({
Expand All @@ -31,6 +33,7 @@ const SafeAppList = ({
removeCustomApp,
title,
query,
isFiltered = false,
}: SafeAppListProps) => {
const { isPreviewDrawerOpen, previewDrawerApp, openPreviewDrawer, closePreviewDrawer } = useSafeAppPreviewDrawer()
const { openedSafeAppIds } = useOpenedSafeApps()
Expand Down Expand Up @@ -69,6 +72,8 @@ const SafeAppList = ({
</li>
))}

{!isFiltered && <NativeSwapsCard />}

{/* Flat list filtered by search query */}
{safeAppsList.map((safeApp) => (
<li key={safeApp.id}>
Expand Down
22 changes: 12 additions & 10 deletions src/features/swap/components/SwapWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import Track from '@/components/common/Track'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'

const SWAP_PROMO_WIDGET_IS_HIDDEN = 'SWAP_PROMO_WIDGET_IS_HIDDEN'
const SWAPS_PROMO_WIDGET_IS_HIDDEN = 'swapsPromoWidgetIsHidden'

function SwapWidget(): ReactElement | null {
const [isHidden = false, setIsHidden] = useLocalStorage<boolean>(SWAP_PROMO_WIDGET_IS_HIDDEN)
const [isHidden = false, setIsHidden] = useLocalStorage<boolean>(SWAPS_PROMO_WIDGET_IS_HIDDEN)
const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS)

const onClick = useCallback(() => {
Expand All @@ -36,40 +36,42 @@ function SwapWidget(): ReactElement | null {
<Card className={css.card}>
<Grid container className={css.grid}>
<Grid item xs>
<Box className={css.wrapper}>
<Box className={css.wrapper} height="100%">
<Box>
<Typography variant="h4" className={css.title}>
Introducing native swaps
</Typography>
<Chip />
</Box>

<Box>
<Box display="flex" flexDirection="column" height="100%">
<Typography mt={1}>
Experience our native swaps, powered by CoW Protocol! Trade seamlessly and efficiently with decoded
transactions that are easy to understand.
</Typography>

<Box flex={1} />

<Box className={css.buttonContainer} mt={3}>
<Track {...SWAP_EVENTS.OPEN_SWAPS} label={SWAP_LABELS.promoWidget}>
<Link
href={{ pathname: AppRoutes.swap, query: { safe: router.query.safe } }}
passHref
legacyBehavior
>
<Button variant="contained" size="small">
Try it now
</Button>
<Button variant="contained">Try it now</Button>
</Link>
</Track>
<Button variant="text" size="small" onClick={onClick}>
<Button variant="text" onClick={onClick}>
Don&apos;t show again
</Button>
</Box>
</Box>
</Box>
</Grid>
<Grid item className={css.imageContainer}>
<img src="/images/common/ic-swaps.svg" alt="Swap" width={400} />

<Grid item xs={6} className={css.imageContainer}>
<img src="/images/common/ic-swaps.svg" alt="Swap" />
</Grid>
</Grid>
</Card>
Expand Down
37 changes: 23 additions & 14 deletions src/features/swap/components/SwapWidget/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

.grid {
display: flex;
flex-wrap: nowrap;
height: inherit;
gap: var(--space-3);
}
Expand All @@ -27,31 +28,39 @@
margin-right: var(--space-1);
}

.imageContainer {
display: flex;
align-items: flex-end;
}

.buttonContainer {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
gap: var(--space-2);
white-space: nowrap;
position: relative;
z-index: 2;
}

@media (max-width: 599.95px) {
.imageContainer {
width: 100%;
justify-content: flex-end;
}
.imageContainer {
position: relative;
z-index: 1;
}

.buttonContainer {
gap: 0;
justify-content: space-between;
}
.imageContainer img {
display: block;
position: absolute;
z-index: 0;
right: 0;
bottom: 0;
max-width: 500px;
}

@media (max-width: 599.95px) {
.wrapper {
padding: var(--space-3);
}
}

@media (max-width: 970px) {
.imageContainer {
display: none;
}
}
1 change: 1 addition & 0 deletions src/pages/apps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const SafeApps: NextPage = () => {
{/* All apps */}
<SafeAppList
title="All apps"
isFiltered={isFiltered}
safeAppsList={isFiltered ? filteredApps : nonPinnedApps}
safeAppsListLoading={remoteSafeAppsLoading}
bookmarkedSafeAppsId={pinnedSafeAppIds}
Expand Down
1 change: 1 addition & 0 deletions src/services/analytics/events/swaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export enum SWAP_LABELS {
asset = 'asset',
dashboard_assets = 'dashboard_assets',
promoWidget = 'promoWidget',
safeAppsPromoWidget = 'safeAppsPromoWidget',
}

0 comments on commit 9720343

Please sign in to comment.