Skip to content

Commit

Permalink
feat: create account management functionality (#60)
Browse files Browse the repository at this point in the history
* feat: create useSign hook

* feat: add react-native-quick-crypto back

* feat: create chainsDisplay component to show the grouped chains images

* chore: move redux provider to the top of the react three to make the bottomSheet provider able to read values from redux selectors

* feat: create badge variant for using the badge component in the chainsDisplay component according to figma

* feat: add testIDs in the chainsDisplay component

* feat: allow user to provider a footer to the dropdown component

* chore: make the logo size dynamically

* feat: create AccountCard component

* feat: create AccountItem component to handle the dropdown events and specific layout

* feat: replace the image component by the chainsDisplay component in the Balances component

* feat: change the color of the active chain in the chain selection dropdown

* feat: create a footer for MyAccounts dropdown

* feat: remove unnecessary tests

* feat: add accounts management feature inside the navbar dropdown

* feat: create useInfiniteScroll hook to handle infinite scroll functionality

* feat: use activeChain information into the tokens container

* feat: make the Identicon component to be more extensible

* feat: add mocked constants inside the store folder

* feat: create safes slice to store all safes added into the app

* feat: add possibility to get all supported chains ids and get them also by id

* chore: auto generated types

* chore: remove unused types

* feat: create MyAccounts container

* feat: use isFetching instead isLoading to avoid cached result while query is being revalidated

* chore: memoize the chains manipulation in the chainsDisplay component

* chore: create an useMyAccountsService hook to handle pos-fetch logic outside the container

(cherry picked from commit be1a137b30984b8fcc866fd6767824f5f67ec659)
  • Loading branch information
clovisdasilvaneto committed Dec 24, 2024
1 parent 1068919 commit 576bd47
Show file tree
Hide file tree
Showing 49 changed files with 2,491 additions and 383 deletions.
3 changes: 2 additions & 1 deletion apps/mobile/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ expo-env.d.ts
# Native
*.orig.*
*.
*.jks
*.p8
*.p12
*.key
*.
*.mobileprovision

# Metro
.metro-health-check*
Expand Down
16 changes: 8 additions & 8 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ function RootLayout() {
store.dispatch(apiSliceWithChainsConfig.endpoints.getChainsConfig.initiate())

return (
<PortalProvider shouldAddRootHost>
<GestureHandlerRootView>
<BottomSheetModalProvider>
<Provider store={store}>
<Provider store={store}>
<PortalProvider shouldAddRootHost>
<GestureHandlerRootView>
<BottomSheetModalProvider>
<PersistGate loading={null} persistor={persistor}>
<SafeThemeProvider>
<SafeToastProvider>
Expand Down Expand Up @@ -63,10 +63,10 @@ function RootLayout() {
</SafeToastProvider>
</SafeThemeProvider>
</PersistGate>
</Provider>
</BottomSheetModalProvider>
</GestureHandlerRootView>
</PortalProvider>
</BottomSheetModalProvider>
</GestureHandlerRootView>
</PortalProvider>
</Provider>
)
}

Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/src/components/Badge/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface BadgeProps {
circleProps?: Partial<CircleProps>
textContentProps?: Partial<TextProps>
circular?: boolean
testID?: string
}

export const Badge = ({
Expand All @@ -25,6 +26,7 @@ export const Badge = ({
circular = true,
circleProps,
textContentProps,
testID,
}: BadgeProps) => {
let contentToRender = content
if (typeof content === 'string') {
Expand All @@ -38,7 +40,7 @@ export const Badge = ({
if (circular) {
return (
<Theme name={themeName}>
<Circle size={circleSize} backgroundColor={'$background'} {...circleProps}>
<Circle testID={testID} size={circleSize} backgroundColor={'$background'} {...circleProps}>
{contentToRender}
</Circle>
</Theme>
Expand All @@ -47,6 +49,7 @@ export const Badge = ({
return (
<Theme name={themeName}>
<View
testID={testID}
alignSelf={'flex-start'}
paddingVertical="$1"
paddingHorizontal="$3"
Expand Down
8 changes: 8 additions & 0 deletions apps/mobile/src/components/Badge/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,12 @@ export const badgeTheme = {
color: tokens.color.warning1ContrastTextDark,
background: tokens.color.warningDarkDark,
},
dark_badge_background: {
color: tokens.color.textPrimaryDark,
background: tokens.color.logoBackgroundDark,
},
light_badge_background: {
color: tokens.color.textPrimaryLight,
background: tokens.color.logoBackgroundLight,
},
}
45 changes: 45 additions & 0 deletions apps/mobile/src/components/ChainsDisplay/ChainsDisplay.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/react'
import { ChainsDisplay } from '@/src/components/ChainsDisplay'
import { mockedChains } from '@/src/store/constants'
import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains'

const meta: Meta<typeof ChainsDisplay> = {
title: 'ChainsDisplay',
component: ChainsDisplay,
argTypes: {},
}

export default meta

type Story = StoryObj<typeof ChainsDisplay>

export const Default: Story = {
args: {
chains: mockedChains as unknown as Chain[],
max: 3,
},
parameters: {
layout: 'fullscreen',
},
}

export const Truncated: Story = {
args: {
chains: mockedChains as unknown as Chain[],
max: 1,
},
parameters: {
layout: 'fullscreen',
},
}

export const ActiveChain: Story = {
args: {
chains: mockedChains as unknown as Chain[],
activeChainId: mockedChains[1].chainId,
max: 1,
},
parameters: {
layout: 'fullscreen',
},
}
30 changes: 30 additions & 0 deletions apps/mobile/src/components/ChainsDisplay/ChainsDisplay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { mockedChains } from '@/src/store/constants'
import { ChainsDisplay } from './ChainsDisplay'
import { render } from '@testing-library/react-native'
import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains'

describe('ChainsDisplay', () => {
it('should render all chains next each other', () => {
const container = render(<ChainsDisplay chains={mockedChains as unknown as Chain[]} max={mockedChains.length} />)

expect(container.getAllByTestId('chain-display')).toHaveLength(3)
})
it('should truncate the chains when the provided chains length is greatter than the max', () => {
const container = render(<ChainsDisplay chains={mockedChains as unknown as Chain[]} max={2} />)
const moreChainsBadge = container.getByTestId('more-chains-badge')

expect(container.getAllByTestId('chain-display')).toHaveLength(2)
expect(moreChainsBadge).toBeVisible()
expect(moreChainsBadge).toHaveTextContent('+1')
})

it('should always show the selected chain as the first column of the row', () => {
const container = render(
<ChainsDisplay chains={mockedChains as unknown as Chain[]} max={2} activeChainId={mockedChains[2].chainId} />,
)

expect(container.getAllByTestId('chain-display')[0].children[0].props.accessibilityLabel).toBe(
mockedChains[2].chainName,
)
})
})
34 changes: 34 additions & 0 deletions apps/mobile/src/components/ChainsDisplay/ChainsDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains'
import React, { useMemo } from 'react'
import { View } from 'tamagui'
import { Logo } from '../Logo'
import { Badge } from '../Badge'

interface ChainsDisplayProps {
chains: Chain[]
max?: number
activeChainId?: string
}

export function ChainsDisplay({ chains, activeChainId, max }: ChainsDisplayProps) {
const orderedChains = useMemo(
() => [...chains].sort((a, b) => (a.chainId === activeChainId ? -1 : b.chainId === activeChainId ? 1 : 0)),
[chains],
)
const slicedChains = max ? orderedChains.slice(0, max) : chains
const showBadge = max && chains.length > max

return (
<View flexDirection="row">
{slicedChains.map(({ chainLogoUri, chainName, chainId }, index) => (
<View key={chainId} testID="chain-display" marginRight={(showBadge || index !== slicedChains.length - 1) && -8}>
<Logo size="$7" logoUri={chainLogoUri} accessibilityLabel={chainName} />
</View>
))}

{showBadge && (
<Badge testID="more-chains-badge" content={`+${chains.length - max}`} themeName="badge_background" />
)}
</View>
)
}
1 change: 1 addition & 0 deletions apps/mobile/src/components/ChainsDisplay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ChainsDisplay } from './ChainsDisplay'
28 changes: 22 additions & 6 deletions apps/mobile/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useRef } from 'react'
import { H5, ScrollView, Text, View } from 'tamagui'
import { GetThemeValueForKey, H5, ScrollView, Text, View } from 'tamagui'
import { SafeFontIcon } from '@/src/components/SafeFontIcon/SafeFontIcon'
import { BottomSheetModal, BottomSheetView } from '@gorhom/bottom-sheet'
import { BottomSheetFooterProps, BottomSheetModal, BottomSheetModalProps, BottomSheetView } from '@gorhom/bottom-sheet'
import { StyleSheet } from 'react-native'
import { BackdropComponent, BackgroundComponent } from './sheetComponents'

Expand All @@ -11,18 +11,32 @@ interface DropdownProps<T> {
children?: React.ReactNode
dropdownTitle?: string
items?: T[]
snapPoints?: BottomSheetModalProps['snapPoints']
labelProps?: {
fontSize?: '$4' | '$5' | GetThemeValueForKey<'fontSize'>
fontWeight: 400 | 500 | 600
}
footerComponent?: React.FC<BottomSheetFooterProps>
renderItem?: React.FC<{ item: T; onClose: () => void }>
keyExtractor?: ({ item, index }: { item: T; index: number }) => string
}

const defaultLabelProps = {
fontSize: '$4',
fontWeight: 400,
} as const

export function Dropdown<T>({
label,
leftNode,
children,
dropdownTitle,
items,
snapPoints = [600, '90%'],
keyExtractor,
renderItem: Render,
labelProps = defaultLabelProps,
footerComponent,
}: DropdownProps<T>) {
const bottomSheetModalRef = useRef<BottomSheetModal>(null)

Expand All @@ -44,10 +58,11 @@ export function Dropdown<T>({
onPress={handlePresentModalPress}
flexDirection="row"
marginBottom="$3"
columnGap="$2"
>
{leftNode}

<Text fontSize="$4" fontWeight={400}>
<Text fontSize={labelProps.fontSize} fontWeight={labelProps.fontWeight}>
{label}
</Text>

Expand All @@ -56,19 +71,20 @@ export function Dropdown<T>({

<BottomSheetModal
enableOverDrag={false}
snapPoints={[400, '90%']}
snapPoints={snapPoints}
enableDynamicSizing={false}
ref={bottomSheetModalRef}
enablePanDownToClose
overDragResistanceFactor={10}
backgroundComponent={BackgroundComponent}
backdropComponent={BackdropComponent}
footerComponent={footerComponent}
>
<BottomSheetView style={styles.contentContainer}>
<ScrollView>
<View minHeight={200} alignItems="center" paddingVertical="$3">
{dropdownTitle && (
<H5 marginBottom="$4" fontWeight={600}>
<H5 marginBottom="$6" fontWeight={600}>
{dropdownTitle}
</H5>
)}
Expand All @@ -95,6 +111,6 @@ export function Dropdown<T>({
const styles = StyleSheet.create({
contentContainer: {
paddingHorizontal: 20,
flex: 1,
justifyContent: 'space-around',
},
})
11 changes: 9 additions & 2 deletions apps/mobile/src/components/Logo/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ interface LogoProps {
accessibilityLabel?: string
fallbackIcon?: IconProps['name']
imageBackground?: string
size?: string
}

export function Logo({ logoUri, accessibilityLabel, imageBackground = '$color', fallbackIcon = 'nft' }: LogoProps) {
export function Logo({
logoUri,
accessibilityLabel,
size = '$10',
imageBackground = '$color',
fallbackIcon = 'nft',
}: LogoProps) {
return (
<Theme name="logo">
<Avatar circular size="$10">
<Avatar circular size={size}>
{logoUri && (
<Avatar.Image
testID="logo-image"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from '@storybook/react'
import { AccountCard } from '@/src/components/transactions-list/Card/AccountCard'
import { mockedActiveSafeInfo, mockedChains } from '@/src/store/constants'
import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains'
import { Address } from '@/src/types/address'
import { SafeFontIcon } from '@/src/components/SafeFontIcon'

const meta: Meta<typeof AccountCard> = {
title: 'TransactionsList/AccountCard',
component: AccountCard,
argTypes: {},
}

export default meta

type Story = StoryObj<typeof AccountCard>

export const Default: Story = {
args: {
name: 'This is my account',
chains: mockedChains as unknown as Chain[],
owners: 5,
balance: mockedActiveSafeInfo.fiatTotal,
address: mockedActiveSafeInfo.address.value as Address,
threshold: 2,
},
parameters: {
layout: 'fullscreen',
},
render: ({ ...args }) => <AccountCard {...args} rightNode={<SafeFontIcon name="check" />} />,
}

export const TruncatedAccount: Story = {
args: {
name: 'This is my account with a very long text in one more test',
chains: mockedChains as unknown as Chain[],
owners: 5,
balance: mockedActiveSafeInfo.fiatTotal,
address: mockedActiveSafeInfo.address.value as Address,
threshold: 2,
},
parameters: {
layout: 'fullscreen',
},
render: ({ ...args }) => <AccountCard {...args} rightNode={<SafeFontIcon name="check" />} />,
}
Loading

0 comments on commit 576bd47

Please sign in to comment.