Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add swaps card to safe apps list #3786

Merged
merged 24 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
49dab7d
feat: add native swap card to storybook
jmealy May 30, 2024
c423b09
make card clickable and handle dismiss
jmealy May 31, 2024
3e1445c
add flag to safe apps card type for native features
jmealy May 31, 2024
77dd4c5
move hook to hooks folder
jmealy May 31, 2024
aac00da
adjust padding
jmealy May 31, 2024
ae9d480
remove test code
jmealy May 31, 2024
11a347c
fix: enable swap app card only on supported networks
compojoom Jun 3, 2024
b1f486e
fix: failing apps test
compojoom Jun 3, 2024
944d1ec
fix lint error and add tags to swaps card
jmealy Jun 4, 2024
38f1106
Merge branch 'dev' into feat/safe-apps-swaps-block
jmealy Jun 4, 2024
af6047a
render the swaps card first
jmealy Jun 10, 2024
7feddc4
Merge branch 'dev' into feat/safe-apps-swaps-block
jmealy Jun 10, 2024
fdb26ea
separate native app promotion from safe app cards
jmealy Jun 10, 2024
f8bf88e
remove swap card hook
jmealy Jun 10, 2024
6ac939b
revert changes to safe apps card map
jmealy Jun 10, 2024
5e5a453
Update src/features/swap/index.tsx
jmealy Jun 10, 2024
0badc49
Update src/pages/apps/index.tsx
jmealy Jun 10, 2024
2d71a13
use localstorage flag within card component and fix storybook
jmealy Jun 10, 2024
69b6427
remove commented code
jmealy Jun 10, 2024
85d543e
revert changes to useCurrentChain
jmealy Jun 10, 2024
19aeebd
remove unused app id
jmealy Jun 10, 2024
aa7f2b9
Merge branch 'dev' of github.com:safe-global/safe-wallet-web into fea…
katspaugh Jun 11, 2024
8da9076
Move SAP banner down
katspaugh Jun 11, 2024
d8f3a18
Adjust banners
katspaugh Jun 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/components/safe-apps/NativeFeatureCard/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from '@storybook/react'
import NativeFeatureCard from './index'
import { Box } from '@mui/material'
import { SafeAppAccessPolicyTypes } from '@safe-global/safe-gateway-typescript-sdk'
import { AppRoutes } from '@/config/routes'

const meta = {
component: NativeFeatureCard,
parameters: {
componentSubtitle: 'Renders an order id with an external link and a copy button',
},

decorators: [
(Story) => {
return (
<Box sx={{ maxWidth: '500px' }}>
<Story />
</Box>
)
},
],
tags: ['autodocs'],
} satisfies Meta<typeof NativeFeatureCard>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
details: {
id: 100_000,
url: AppRoutes.swap,
name: 'Native swaps are here!',
description: 'Experience seamless trading with better decoding and security in native swaps.',
accessControl: { type: SafeAppAccessPolicyTypes.NoRestrictions },
tags: ['DeFi'],
features: [],
socialProfiles: [],
developerWebsite: '',
chainIds: ['11155111'],
iconUrl: '/images/common/swap.svg',
},
onClick: () => {},
onDismiss: () => {},
},
}
49 changes: 49 additions & 0 deletions src/components/safe-apps/NativeFeatureCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 type { SafeAppData } from '@safe-global/safe-gateway-typescript-sdk'
import SafeAppIconCard from '../SafeAppIconCard'
import css from './styles.module.css'

type NativeFeatureCardProps = {
details: SafeAppData
onClick: () => void
onDismiss: () => void
}

const NativeFeatureCard = ({ details, onClick, onDismiss }: NativeFeatureCardProps) => {
return (
<Paper className={css.container}>
<CardHeader
className={css.header}
avatar={
<div className={css.iconContainer}>
<SafeAppIconCard src={details.iconUrl} alt={details.name} width={24} height={24} />
</div>
}
/>

<CardContent className={css.content}>
<Typography className={css.title} variant="h5">
{details.name}
</Typography>

<Typography className={css.description} variant="body2" color="text.secondary">
{details.description}
</Typography>

<Stack direction="row" gap={2} className={css.buttons}>
<Button onClick={onClick} size="small" variant="contained" sx={{ px: '16px' }}>
katspaugh marked this conversation as resolved.
Show resolved Hide resolved
Try now
</Button>
<Button onClick={onDismiss} size="small" variant="text" sx={{ px: '16px' }}>
Don&apos;t show
</Button>
</Stack>
</CardContent>
</Paper>
)
}

export default NativeFeatureCard
49 changes: 49 additions & 0 deletions src/components/safe-apps/NativeFeatureCard/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.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);
}
50 changes: 38 additions & 12 deletions src/components/safe-apps/SafeAppList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ 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 NativeFeatureCard from '../NativeFeatureCard'
import { useNativeSwapsAppCard } from '../hooks/useNativeSwapsAppCard'
import { useRouter } from 'next/router'
import { NATIVE_SWAPS_APP_ID } from '@/features/swap/config/constants'

type SafeAppListProps = {
safeAppsList: SafeAppData[]
Expand All @@ -34,6 +38,8 @@ const SafeAppList = ({
}: SafeAppListProps) => {
const { isPreviewDrawerOpen, previewDrawerApp, openPreviewDrawer, closePreviewDrawer } = useSafeAppPreviewDrawer()
const { openedSafeAppIds } = useOpenedSafeApps()
const { isVisible, setIsVisible } = useNativeSwapsAppCard()
const router = useRouter()

const showZeroResultsPlaceholder = query && safeAppsList.length === 0

Expand All @@ -48,6 +54,16 @@ const SafeAppList = ({
[openPreviewDrawer, openedSafeAppIds],
)

const handleNativeAppClick = useCallback(
(route: string) => {
router.push({
pathname: route,
query: router.query,
})
},
[router],
)
katspaugh marked this conversation as resolved.
Show resolved Hide resolved

return (
<>
{/* Safe Apps List Header */}
Expand All @@ -70,18 +86,28 @@ const SafeAppList = ({
))}

{/* Flat list filtered by search query */}
{safeAppsList.map((safeApp) => (
<li key={safeApp.id}>
<SafeAppCard
safeApp={safeApp}
isBookmarked={bookmarkedSafeAppsId?.has(safeApp.id)}
onBookmarkSafeApp={onBookmarkSafeApp}
removeCustomApp={removeCustomApp}
onClickSafeApp={handleSafeAppClick(safeApp)}
openPreviewDrawer={openPreviewDrawer}
/>
</li>
))}
{safeAppsList.map((safeApp) => {
return safeApp.id !== NATIVE_SWAPS_APP_ID ? (
<li key={safeApp.id}>
<SafeAppCard
safeApp={safeApp}
isBookmarked={bookmarkedSafeAppsId?.has(safeApp.id)}
onBookmarkSafeApp={onBookmarkSafeApp}
removeCustomApp={removeCustomApp}
onClickSafeApp={handleSafeAppClick(safeApp)}
openPreviewDrawer={openPreviewDrawer}
/>
</li>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this card have to be added into the safeAppsList? Why can't it just be rendered outside of the map? Feels very hacky to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It gets mixed in with the safe apps in the parent component just so that it responds to filtering.
I changed it here to render outside of the map, hopefully it's a bit better

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's totally fine if it disappears when the list is filtered.

) : isVisible ? (
<li key={safeApp.id}>
<NativeFeatureCard
details={safeApp}
onClick={() => handleNativeAppClick(safeApp.url)}
onDismiss={() => setIsVisible(false)}
/>
</li>
) : null
})}
</ul>

{/* Zero results placeholder */}
Expand Down
40 changes: 40 additions & 0 deletions src/components/safe-apps/hooks/useNativeSwapsAppCard.ts
katspaugh marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { AppRoutes } from '@/config/routes'
import useChainId from '@/hooks/useChainId'
import useLocalStorage from '@/services/local-storage/useLocalStorage'
import type { SafeAppData } from '@safe-global/safe-gateway-typescript-sdk'
import { SafeAppAccessPolicyTypes, SafeAppFeatures } from '@safe-global/safe-gateway-typescript-sdk'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
import { NATIVE_SWAPS_APP_ID } from '@/features/swap/config/constants'

const SWAPS_APP_CARD_STORAGE_KEY = 'showSwapsAppCard'

export function useNativeSwapsAppCard() {
const chainId = useChainId()
let [isVisible = true, setIsVisible] = useLocalStorage<boolean>(SWAPS_APP_CARD_STORAGE_KEY)
const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS)

if (isVisible && !isSwapFeatureEnabled) {
isVisible = false
}

const swapsCard: SafeAppData = {
id: NATIVE_SWAPS_APP_ID,
url: AppRoutes.swap,
name: 'Native swaps are here!',
description: 'Experience seamless trading with better decoding and security in native swaps.',
accessControl: { type: SafeAppAccessPolicyTypes.NoRestrictions },
tags: ['DeFi', 'DEX'],
features: [SafeAppFeatures.BATCHED_TRANSACTIONS],
socialProfiles: [],
developerWebsite: '',
chainIds: [chainId],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here I have the card shown on whatever chain is selected. Is swaps enabled on all chains?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you can use: const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) to determine if swaps are enabled on the current chain.
Also how about we don't go for a custom "isNativeFeature" flag, but instead expose the swap app id as a custom const NATIVE_SWAP_APP_ID = 100_000
and then later you can compare against that id, instead of taht new isNativeFeature flag.

iconUrl: '/images/common/swap.svg',
}

return {
swapsCardDetails: swapsCard,
isVisible,
setIsVisible,
}
}
1 change: 1 addition & 0 deletions src/features/swap/config/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const NATIVE_SWAPS_APP_ID = 100_000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const NATIVE_SWAPS_APP_ID = 100_000

3 changes: 2 additions & 1 deletion src/features/swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { isBlockedAddress } from '@/services/ofac'
import { selectSwapParams, setSwapParams, type SwapState } from './store/swapParamsSlice'
import { setSwapOrder } from '@/store/swapOrderSlice'
import useChainId from '@/hooks/useChainId'
import { NATIVE_SWAPS_APP_ID } from '@/features/swap/config/constants'
katspaugh marked this conversation as resolved.
Show resolved Hide resolved

const BASE_URL = typeof window !== 'undefined' && window.location.origin ? window.location.origin : ''

Expand Down Expand Up @@ -75,7 +76,7 @@ const SwapWidget = ({ sell }: Params) => {

const appData: SafeAppData = useMemo(
() => ({
id: 1,
id: NATIVE_SWAPS_APP_ID,
jmealy marked this conversation as resolved.
Show resolved Hide resolved
url: 'https://app.safe.global',
name: SWAP_TITLE,
iconUrl: darkMode ? './images/common/safe-swap-dark.svg' : './images/common/safe-swap.svg',
Expand Down
13 changes: 8 additions & 5 deletions src/pages/apps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ import useSafeAppsFilters from '@/hooks/safe-apps/useSafeAppsFilters'
import SafeAppsFilters from '@/components/safe-apps/SafeAppsFilters'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
import { useNativeSwapsAppCard } from '@/components/safe-apps/hooks/useNativeSwapsAppCard'

const SafeApps: NextPage = () => {
const router = useRouter()
const { swapsCardDetails } = useNativeSwapsAppCard()
const { remoteSafeApps, remoteSafeAppsLoading, pinnedSafeApps, pinnedSafeAppIds, togglePin } = useSafeApps()
const allApps = useMemo(() => [swapsCardDetails, ...remoteSafeApps], [remoteSafeApps, swapsCardDetails])
katspaugh marked this conversation as resolved.
Show resolved Hide resolved
const { filteredApps, query, setQuery, setSelectedCategories, setOptimizedWithBatchFilter, selectedCategories } =
useSafeAppsFilters(remoteSafeApps)
const isFiltered = filteredApps.length !== remoteSafeApps.length
useSafeAppsFilters(allApps)
const isFiltered = filteredApps.length !== allApps.length
const isSafeAppsEnabled = useHasFeature(FEATURES.SAFE_APPS)

const nonPinnedApps = useMemo(
() => remoteSafeApps.filter((app) => !pinnedSafeAppIds.has(app.id)),
[remoteSafeApps, pinnedSafeAppIds],
() => allApps.filter((app) => !pinnedSafeAppIds.has(app.id)),
[allApps, pinnedSafeAppIds],
)

// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -57,7 +60,7 @@ const SafeApps: NextPage = () => {
onChangeFilterCategory={setSelectedCategories}
onChangeOptimizedWithBatch={setOptimizedWithBatchFilter}
selectedCategories={selectedCategories}
safeAppsList={remoteSafeApps}
safeAppsList={allApps}
/>

{/* Pinned apps */}
Expand Down
Loading