Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions apps/mobile/__mocks__/react-native-collapsible-tab-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react'
import { View, FlatList } from 'react-native'

export const Tabs = {
Container: ({ children, renderTabBar }) => (
<View>
{renderTabBar && renderTabBar({ index: 0, routes: [] })}
{children}
</View>
),
Tab: ({ children }: { children: React.ReactNode }) => <View>{children}</View>,
FlashList: FlatList,
FlatList: FlatList,
useTabsContext: () => ({
focusedTab: '',
tabNames: [],
index: 0,
routes: [],
jumpTo: jest.fn(),
}),
useTabNameContext: () => ({ tabName: 'Tokens' }),
ScrollView: ({ children }: { children: React.ReactNode }) => <View testID="fallback">{children}</View>,
}

export default Tabs
12 changes: 6 additions & 6 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ function RootLayout() {
store.dispatch(apiSliceWithChainsConfig.endpoints.getChainsConfig.initiate())

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

Expand Down
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"react-native-collapsible-tab-view": "^8.0.0",
"react-native-device-crypto": "^0.1.7",
"react-native-device-info": "^14.0.1",
"react-native-draggable-flatlist": "^4.0.1",
"react-native-gesture-handler": "~2.20.2",
"react-native-keychain": "^9.2.2",
"react-native-mmkv": "^3.1.0",
Expand Down
12 changes: 9 additions & 3 deletions apps/mobile/src/components/Container/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ const StyledYStack = styled(YStack, {
})

export const Container = (
props: YStackProps & { bordered?: boolean; transparent?: boolean; themeName?: ThemeName },
props: YStackProps & { bordered?: boolean; spaced?: boolean; transparent?: boolean; themeName?: ThemeName },
) => {
const { children, bordered, themeName = 'container', ...rest } = props
const { children, bordered, themeName = 'container', spaced = true, ...rest } = props
return (
<Theme name={themeName}>
<StyledYStack bordered={!!bordered} borderRadius={'$3'} paddingHorizontal={'$4'} paddingVertical={'$4'} {...rest}>
<StyledYStack
bordered={!!bordered}
borderRadius={'$3'}
paddingHorizontal={spaced ? '$4' : 0}
paddingVertical={'$4'}
{...rest}
>
{children}
</StyledYStack>
</Theme>
Expand Down
90 changes: 66 additions & 24 deletions apps/mobile/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import React, { useCallback, useRef } from 'react'
import React, { useCallback, useMemo, useRef } from 'react'
import { GetThemeValueForKey, H5, ScrollView, Text, View } from 'tamagui'
import { SafeFontIcon } from '@/src/components/SafeFontIcon/SafeFontIcon'
import { BottomSheetFooterProps, BottomSheetModal, BottomSheetModalProps, BottomSheetView } from '@gorhom/bottom-sheet'
import { StyleSheet } from 'react-native'
import { BackdropComponent, BackgroundComponent } from './sheetComponents'

import DraggableFlatList, { DragEndParams, RenderItemParams, ScaleDecorator } from 'react-native-draggable-flatlist'

interface DropdownProps<T> {
label: string
leftNode?: React.ReactNode
children?: React.ReactNode
dropdownTitle?: string
sortable?: boolean
onDragEnd?: (params: DragEndParams<T>) => void
items?: T[]
snapPoints?: BottomSheetModalProps['snapPoints']
labelProps?: {
fontSize?: '$4' | '$5' | GetThemeValueForKey<'fontSize'>
fontWeight: 400 | 500 | 600
}
actions?: React.ReactNode
footerComponent?: React.FC<BottomSheetFooterProps>
renderItem?: React.FC<{ item: T; onClose: () => void }>
renderItem?: React.FC<{ item: T; isDragging?: boolean; drag?: () => void; onClose: () => void }>
keyExtractor?: ({ item, index }: { item: T; index: number }) => string
}

Expand All @@ -31,15 +36,17 @@ export function Dropdown<T>({
leftNode,
children,
dropdownTitle,
sortable,
items,
snapPoints = [600, '90%'],
keyExtractor,
actions,
renderItem: Render,
labelProps = defaultLabelProps,
footerComponent,
onDragEnd,
}: DropdownProps<T>) {
const bottomSheetModalRef = useRef<BottomSheetModal>(null)

const handlePresentModalPress = useCallback(() => {
bottomSheetModalRef.current?.present()
}, [])
Expand All @@ -49,6 +56,33 @@ export function Dropdown<T>({
}, [])

const hasCustomItems = items && Render
const isSortable = items && sortable

const renderItem = useCallback(
({ item, drag, isActive }: RenderItemParams<T>) => {
return (
<ScaleDecorator activeScale={1.05}>
{Render && <Render drag={drag} isDragging={isActive} item={item} onClose={handleModalClose} />}
</ScaleDecorator>
)
},
[handleModalClose, Render],
)

const renderDropdownHeader = useMemo(
() => (
<View justifyContent="center" marginTop="$3" marginBottom="$4" alignItems="center">
<H5 fontWeight={600}>{dropdownTitle}</H5>

{actions && (
<View position="absolute" right={'$4'} top={'$1'}>
{actions}
</View>
)}
</View>
),
[dropdownTitle, actions],
)

return (
<>
Expand Down Expand Up @@ -80,28 +114,37 @@ export function Dropdown<T>({
backdropComponent={BackdropComponent}
footerComponent={footerComponent}
>
<BottomSheetView style={styles.contentContainer}>
<ScrollView>
<View minHeight={200} alignItems="center" paddingVertical="$3">
{dropdownTitle && (
<H5 marginBottom="$6" fontWeight={600}>
{dropdownTitle}
</H5>
)}
{!isSortable && dropdownTitle && renderDropdownHeader}

<View alignItems="flex-start" paddingBottom="$4" width="100%">
{hasCustomItems
? items.map((item, index) => (
<Render
key={keyExtractor ? keyExtractor({ item, index }) : index}
item={item}
onClose={handleModalClose}
/>
))
: children}
<BottomSheetView
style={[styles.contentContainer, !isSortable ? { flex: 1, paddingHorizontal: 20 } : undefined]}
>
{isSortable ? (
<DraggableFlatList<T>
data={items}
containerStyle={{ height: '100%' }}
ListHeaderComponent={dropdownTitle ? renderDropdownHeader : undefined}
onDragEnd={onDragEnd}
keyExtractor={(item, index) => (keyExtractor ? keyExtractor({ item, index }) : index.toString())}
renderItem={renderItem}
/>
) : (
<ScrollView>
<View minHeight={200} alignItems="center" paddingVertical="$3">
<View alignItems="flex-start" paddingBottom="$4" width="100%">
{hasCustomItems
? items.map((item, index) => (
<Render
key={keyExtractor ? keyExtractor({ item, index }) : index}
item={item}
onClose={handleModalClose}
/>
))
: children}
</View>
</View>
</View>
</ScrollView>
</ScrollView>
)}
</BottomSheetView>
</BottomSheetModal>
</>
Expand All @@ -110,7 +153,6 @@ export function Dropdown<T>({

const styles = StyleSheet.create({
contentContainer: {
paddingHorizontal: 20,
justifyContent: 'space-around',
},
})
3 changes: 3 additions & 0 deletions apps/mobile/src/components/SafeListItem/SafeListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface SafeListItemProps {
leftNode?: React.ReactNode
bordered?: boolean
transparent?: boolean
spaced?: boolean
inQueue?: boolean
executionInfo?: Transaction['executionInfo']
themeName?: ThemeName
Expand All @@ -26,6 +27,7 @@ export function SafeListItem({
leftNode,
icon,
bordered,
spaced,
label,
transparent,
rightNode,
Expand All @@ -36,6 +38,7 @@ export function SafeListItem({
}: SafeListItemProps) {
return (
<Container
spaced={spaced}
bordered={bordered}
gap={12}
transparent={transparent}
Expand Down
11 changes: 11 additions & 0 deletions apps/mobile/src/components/Tab/TabNameContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext, useContext } from 'react'

export const TabNameContext = createContext<{ tabName: string }>({ tabName: '' })

export const useTabNameContext = () => {
const context = useContext(TabNameContext)
if (!context) {
throw new Error('useTabNameContext must be inside a TabNameContext')
}
return context
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,25 @@ interface AccountCardProps {
owners: number
threshold: number
rightNode?: string | React.ReactNode
chains: Chain[]
leftNode?: React.ReactNode
chains?: Chain[]
spaced?: boolean
}

export function AccountCard({ name, chains, owners, balance, address, threshold, rightNode }: AccountCardProps) {
export function AccountCard({
name,
chains,
spaced,
owners,
leftNode,
balance,
address,
threshold,
rightNode,
}: AccountCardProps) {
return (
<SafeListItem
spaced={spaced}
label={
<View>
<Text fontSize="$4" fontWeight={600}>
Expand All @@ -31,18 +44,20 @@ export function AccountCard({ name, chains, owners, balance, address, threshold,
</View>
}
leftNode={
<View marginRight="$2">
<View marginRight="$2" flexDirection="row" gap="$2" justifyContent="center" alignItems="center">
{leftNode}
<IdenticonWithBadge
testID="threshold-info-badge"
size={40}
fontSize={owners > 9 ? 8 : 12}
address={address}
badgeContent={`${threshold}/${owners}`}
/>
</View>
}
rightNode={
<View columnGap="$2" flexDirection="row">
<ChainsDisplay chains={chains} max={3} />
{chains && <ChainsDisplay chains={chains} max={3} />}
{rightNode}
</View>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ import { mockERC20Transfer, mockListItemByType, mockNFTTransfer, mockSwapTransfe
import { TransactionListItemType, TransactionStatus } from '@safe-global/store/gateway/types'
import { TransactionItem } from '@safe-global/store/gateway/AUTO_GENERATED/transactions'

jest.mock('@/src/store/chains', () => {
const actualModule = jest.requireActual('@/src/store/chains') // Import the real module
return {
...actualModule,
selectChainById: jest.fn().mockImplementation(() => ({
decimals: 8,
logoUri: 'http://safe.com/logo.png',
name: 'mocked currency',
symbol: 'MCC',
})),
}
})

describe('TxGroupedCard', () => {
it('should render the default markup', () => {
const { getAllByTestId } = render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {
isTxQueued,
} from '@/src/utils/transaction-guards'
import { ellipsis, formatValue } from '@/src/utils/formatters'
import { useSelector } from 'react-redux'
import { selectNativeCurrency } from '@/src/store/activeChainSlice'
import { TransferDirection } from '@safe-global/store/gateway/types'
import { TransferTransactionInfo, Transaction } from '@safe-global/store/gateway/AUTO_GENERATED/transactions'
import { Logo } from '@/src/components/Logo'
import { selectActiveChainCurrency } from '@/src/store/chains'
import { useAppSelector } from '@/src/store/hooks'

interface TxTokenCardProps {
bordered?: boolean
Expand All @@ -34,7 +34,7 @@ interface tokenDetails {
const getTokenDetails = (txInfo: TransferTransactionInfo): tokenDetails => {
const transfer = txInfo.transferInfo
const unnamedToken = 'Unnamed token'
const nativeCurrency = useSelector(selectNativeCurrency)
const nativeCurrency = useAppSelector(selectActiveChainCurrency)

if (isNativeTokenTransfer(transfer)) {
return {
Expand Down
Loading
Loading