From 0978fed06c60a9dfacf2ec30b2c10a080c2f33c3 Mon Sep 17 00:00:00 2001 From: jorbuedo Date: Wed, 4 Dec 2024 14:51:40 +0100 Subject: [PATCH] wip --- .../ExpandableInfoCard.stories.tsx | 178 ---------- .../ExpandableInfoCard/ExpandableInfoCard.tsx | 261 --------------- .../src/features/Swap/common/SwapProvider.tsx | 9 + .../Swap/useCases/ListOrders/ListOrders.tsx | 309 ++++++++++++++++-- .../src/adapters/api/muesliswap/api-maker.ts | 51 +-- yarn.lock | 5 - 6 files changed, 305 insertions(+), 508 deletions(-) delete mode 100644 apps/wallet-mobile/src/components/ExpandableInfoCard/ExpandableInfoCard.stories.tsx delete mode 100644 apps/wallet-mobile/src/components/ExpandableInfoCard/ExpandableInfoCard.tsx diff --git a/apps/wallet-mobile/src/components/ExpandableInfoCard/ExpandableInfoCard.stories.tsx b/apps/wallet-mobile/src/components/ExpandableInfoCard/ExpandableInfoCard.stories.tsx deleted file mode 100644 index 8c78b5dc68..0000000000 --- a/apps/wallet-mobile/src/components/ExpandableInfoCard/ExpandableInfoCard.stories.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import {action} from '@storybook/addon-actions' -import {storiesOf} from '@storybook/react-native' -import React from 'react' -import {StyleSheet, Text, View} from 'react-native' - -import {Icon} from '../Icon' -import {Spacer} from '../Spacer/Spacer' -import {ExpandableInfoCard, Footer, HeaderWrapper, HiddenInfoWrapper, MainInfoWrapper} from './ExpandableInfoCard' - -const styles = StyleSheet.create({ - container: { - flex: 1, - padding: 16, - }, -}) -storiesOf('Expandable Info Card', module) - .addDecorator((story) => {story()}) - .add('with Box Shadow', () => { - const [hiddenInfoOpenId, setHiddenInfoOpenId] = React.useState(null) - - const expanded = hiddenInfoOpenId === '1234' - - return ( - null} />} - expanded={expanded} - header={ -
setHiddenInfoOpenId(hiddenInfoOpenId !== '1234' ? '1234' : null)} - /> - } - footer={ -
action('footer clicked')}> - FOOTER LABEL -
- } - withBoxShadow - > - - - ) - }) - .add('without Box Shadow', () => { - const [hiddenInfoOpenId, setHiddenInfoOpenId] = React.useState(null) - - const expanded = hiddenInfoOpenId === '1234' - - return ( - null} />} - expanded={expanded} - header={ -
setHiddenInfoOpenId(hiddenInfoOpenId !== '1234' ? '1234' : null)} - /> - } - footer={ -
action('footer clicked')}> - FOOTER LABEL -
- } - > - - - ) - }) - .add('with footer disabled', () => { - return ( - null} />} - expanded={false} - header={
null} />} - footer={ -
action('footer clicked')}> - FOOTER LABEL -
- } - > - - - ) - }) - -const Header = ({ - assetFromLabel, - assetToLabel, - expanded, - onPress, -}: { - assetFromLabel: string - assetToLabel: string - expanded: boolean - onPress: () => void -}) => { - return ( - - - - - - - {assetFromLabel} - - / - - - - - - - - {assetToLabel} - - - ) -} - -type BottomSheetState = { - openId: string | null - title: string - content: string -} - -const HiddenInfo = ({id, setBottomSheetState}: {id: string; setBottomSheetState(state: BottomSheetState): void}) => { - return ( - - {[ - { - label: 'Min ADA', - value: '2 ADA', - info: 'Fake content', - }, - { - label: 'Min Received', - value: '2.99 USDA', - info: 'Fake content', - }, - { - label: 'Fees', - value: '2 ADA', - info: 'Fake content', - }, - ].map((item) => ( - { - setBottomSheetState({ - openId: id, - title: item.label, - content: item.info, - }) - }} - /> - ))} - - ) -} - -const MainInfo = () => { - return ( - - {[ - {label: 'Token price', value: '3 ADA'}, - {label: 'Token amount', value: '3 USDA'}, - ].map((item, index) => ( - - ))} - - ) -} diff --git a/apps/wallet-mobile/src/components/ExpandableInfoCard/ExpandableInfoCard.tsx b/apps/wallet-mobile/src/components/ExpandableInfoCard/ExpandableInfoCard.tsx deleted file mode 100644 index ea36d4b739..0000000000 --- a/apps/wallet-mobile/src/components/ExpandableInfoCard/ExpandableInfoCard.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import {isString} from '@yoroi/common' -import {useTheme} from '@yoroi/theme' -import React from 'react' -import {StyleSheet, Text, View} from 'react-native' -import {TouchableOpacity} from 'react-native-gesture-handler' -import SkeletonPlaceholder from 'react-native-skeleton-placeholder' - -import {Icon} from '../Icon' -import {Space} from '../Space/Space' -import {Spacer} from '../Spacer/Spacer' - -type ExpandableInfoCardProps = { - info: React.ReactNode - expanded?: boolean - children?: React.ReactNode - header: React.ReactNode - footer?: React.ReactNode - withBoxShadow?: boolean -} - -export const ExpandableInfoCard = ({ - children, - expanded, - info, - header, - withBoxShadow = false, - footer, -}: ExpandableInfoCardProps) => { - const {styles} = useStyles() - - return ( - - - {header} - - {children !== undefined && ( - <> - - - {children} - - )} - - {expanded && ( - <> - - - {info} - - )} - - {footer !== undefined && ( - <> - - - {footer} - - )} - - - - - ) -} - -export const HeaderWrapper = ({ - children, - expanded, - onPress, -}: { - children: React.ReactNode - expanded?: boolean - onPress: () => void -}) => { - const {styles, colors} = useStyles() - - return ( - - - {children} - - {expanded ? ( - - ) : ( - - )} - - - ) -} - -export const Footer = ({ - children, - onPress, - disabled, -}: { - children: React.ReactNode - onPress: () => void - disabled?: boolean -}) => { - const {styles} = useStyles() - return ( - - {children} - - ) -} - -export const HiddenInfoWrapper = ({ - label, - info, - onPress, - value, - icon, -}: { - label: string - info?: React.ReactNode - onPress?: () => void - value: React.ReactNode - icon?: React.ReactNode -}) => { - const {styles, colors} = useStyles() - return ( - - - - {label} - - - - {info !== undefined && ( - - - - )} - - - - - {isString(value) ? ( - - {icon !== undefined && ( - - {icon} - - - - )} - - {value} - - ) : ( - value - )} - - - - - ) -} - -export const MainInfoWrapper = ({label, value, isLast = false}: {label: string; value?: string; isLast?: boolean}) => { - const {styles} = useStyles() - return ( - - - {label} - - {isString(value) && {value}} - - - {!isLast && } - - ) -} - -export const ExpandableInfoCardSkeleton = () => { - const {colors} = useStyles() - - return ( - - - - ) -} - -const useStyles = () => { - const {color, atoms} = useTheme() - const styles = StyleSheet.create({ - container: { - ...atoms.p_lg, - borderColor: color.gray_200, - backgroundColor: color.bg_color_max, - borderRadius: 8, - borderWidth: 1, - width: '100%', - height: 'auto', - }, - shadowProp: { - backgroundColor: color.bg_color_max, - shadowColor: color.gray_max, - shadowOffset: { - width: 0, - height: 1, - }, - shadowOpacity: 0.2, - shadowRadius: 1.41, - - elevation: 2, - borderWidth: 0, - }, - flexBetween: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - flex: { - flexDirection: 'row', - alignItems: 'center', - }, - text: { - textAlign: 'left', - ...atoms.body_1_lg_regular, - color: color.gray_max, - }, - gray: { - color: color.gray_600, - ...atoms.body_1_lg_regular, - }, - buttonLabel: { - paddingTop: 13, - color: color.gray_max, - ...atoms.body_2_md_medium, - }, - info: { - flexDirection: 'row', - justifyContent: 'space-between', - }, - value: { - textAlign: 'right', - flexShrink: 1, - ...atoms.body_1_lg_regular, - color: color.gray_900, - }, - label: { - color: color.gray_600, - paddingRight: 8, - ...atoms.body_1_lg_regular, - }, - footerDisabled: { - opacity: 0.5, - }, - }) - - const colors = { - skeletonBackground: color.gray_200, - gray: color.gray_max, - } - - return {styles, colors} as const -} diff --git a/apps/wallet-mobile/src/features/Swap/common/SwapProvider.tsx b/apps/wallet-mobile/src/features/Swap/common/SwapProvider.tsx index 0087509aba..b872dcb882 100644 --- a/apps/wallet-mobile/src/features/Swap/common/SwapProvider.tsx +++ b/apps/wallet-mobile/src/features/Swap/common/SwapProvider.tsx @@ -32,6 +32,12 @@ export const SwapProvider = ({children}: {children: React.ReactNode}) => { }) }, [network, stakingKey, address, addressHex, wallet.portfolioPrimaryTokenInfo]) + const {data: orders = []} = useQuery([network, stakingKey], async () => { + const res = await swapManager.api.orders() + if (res.tag === 'right') return res.value.data + return [] + }) + const {data: tokenIds = []} = useQuery([network], async () => { const res = await swapManager.api.tokens() if (res.tag === 'right') return res.value.data.map(({id}) => id) @@ -66,6 +72,7 @@ export const SwapProvider = ({children}: {children: React.ReactNode}) => { wantedPriceInputRef, slippageInputRef, api: swapManager.api, + orders, dispatch, }), [state, swapManager.api, tokenInfos], @@ -261,6 +268,7 @@ type SwapContext = SwapState & { tokenOutInputRef: React.RefObject | undefined wantedPriceInputRef: React.RefObject | undefined slippageInputRef: React.RefObject | undefined + orders?: Array dispatch: React.Dispatch } @@ -271,5 +279,6 @@ const SwapContext = React.createContext({ tokenOutInputRef: undefined, wantedPriceInputRef: undefined, slippageInputRef: undefined, + orders: undefined, dispatch: () => null, }) diff --git a/apps/wallet-mobile/src/features/Swap/useCases/ListOrders/ListOrders.tsx b/apps/wallet-mobile/src/features/Swap/useCases/ListOrders/ListOrders.tsx index 4ceffc86eb..ced741d6e2 100644 --- a/apps/wallet-mobile/src/features/Swap/useCases/ListOrders/ListOrders.tsx +++ b/apps/wallet-mobile/src/features/Swap/useCases/ListOrders/ListOrders.tsx @@ -1,28 +1,33 @@ import {useTheme} from '@yoroi/theme' +import {Portfolio, Swap} from '@yoroi/types' import React from 'react' import {ErrorBoundary} from 'react-error-boundary' -import {StyleSheet, View} from 'react-native' +import {useIntl} from 'react-intl' +import {FlatList, Linking, StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import SkeletonPlaceholder from 'react-native-skeleton-placeholder' import {Boundary} from '../../../../components/Boundary/Boundary' import {Button, ButtonType} from '../../../../components/Button/Button' +import {Icon} from '../../../../components/Icon' import {useWalletNavigation} from '../../../../kernel/navigation' -import {useSearchOnNavBar} from '../../../Search/SearchContext' +import {TokenInfoIcon} from '../../../Portfolio/common/TokenAmountItem/TokenInfoIcon' +import {useSearch, useSearchOnNavBar} from '../../../Search/SearchContext' +import {useWalletManager} from '../../../WalletManager/context/WalletManagerProvider' +import {Counter} from '../../common/Counter/Counter' +import {EmptyCompletedOrdersIllustration} from '../../common/Illustrations/EmptyCompletedOrdersIllustration' +import {EmptyOpenOrdersIllustration} from '../../common/Illustrations/EmptyOpenOrdersIllustration' import {ServiceUnavailable} from '../../common/ServiceUnavailable/ServiceUnavailable' import {useStrings} from '../../common/strings' -// import {CompletedOrders, CompletedOrdersSkeleton} from './CompletedOrders' -// import {OpenOrders, OpenOrdersSkeleton} from './OpenOrders' -// TODO -const CompletedOrders = () => null -const CompletedOrdersSkeleton = () => null -const OpenOrders = () => null -const OpenOrdersSkeleton = () => null +import {useSwap} from '../../common/SwapProvider' + +type Filter = 'open' | 'completed' export const ListOrders = () => { const {navigateToTxHistory} = useWalletNavigation() - const [filter, setFilter] = React.useState<'open' | 'completed'>('open') + const [filter, setFilter] = React.useState('open') const strings = useStrings() - const styles = useStyles() + const {styles, color} = useStyles() useSearchOnNavBar({ placeholder: strings.searchTokens, @@ -55,22 +60,204 @@ export const ListOrders = () => { + + {[0, 1, 2, 3].map((index) => ( + + + + + + ))} + + ), + }} + > + } + > + + + + + ) +} + +const Content = ({filter}: {filter: Filter}) => { + const strings = useStrings() + const {styles} = useStyles() + const {visible: isSearching} = useSearch() + const swapForm = useSwap() + const orders = swapForm.orders?.filter( + ({status}) => (status === 'open' && filter === 'open') || (status !== 'open' && filter === 'completed'), + ) + + return ( + + + } + keyExtractor={(item) => item.txHash ?? item.customId ?? ''} + ListEmptyComponent={} + /> + + + {!isSearching && ( + + )} + + ) +} + +const tokenName = (token?: Portfolio.Token.Info) => token?.name ?? token?.ticker ?? token?.id ?? '-' + +const Order = ({data}: {data: Swap.Order}) => { + const intl = useIntl() + const strings = useStrings() + const { + selected: { + networkManager: {explorers}, + }, + } = useWalletManager() + const {styles, color} = useStyles() + const [expanded, setExpanded] = React.useState(false) + const swapForm = useSwap() + const tokenInInfo = swapForm.tokenInfos.get(data.tokenIn) + const tokenOutInfo = swapForm.tokenInfos.get(data.tokenOut) + + const amountOut = data.actualAmountOut === 0 ? data.expectedAmountOut : data.actualAmountOut + const price = amountOut === 0 ? 0 : data.amountIn / amountOut + + const amountOutStr = String(Number(amountOut.toFixed(tokenOutInfo?.decimals ?? 0))) + const priceStr = String(Number(price.toFixed(tokenOutInfo?.decimals ?? 0))) // TODO + + const lastTxHash = data.updateTxHash ?? data.txHash ?? '' + const shortenedTxHash = `${lastTxHash.substring(0, 9)}...${lastTxHash.substring( + lastTxHash.length - 4, + lastTxHash.length, + )}` + + return ( + + setExpanded(!expanded)}> + + + + + {tokenName(tokenInInfo)} + + / + + + + {tokenName(tokenOutInfo)} + + + + + + + + + + + + {data.placedAt !== undefined && ( + + )} + + {data.lastUpdate !== undefined && ( + + )} + + {expanded && ( + + + + + + {lastTxHash !== '' && ( + Linking.openURL(explorers.cexplorer.tx(lastTxHash))} + title={shortenedTxHash} + /> + } + /> + )} + + )} + + + ) +} + +const Row = ({label, value}: {label: string; value: string | React.ReactNode}) => { + const {styles} = useStyles() + + return ( + + {label} + + {typeof value === 'string' ? {value} : value} + + ) +} + +const ListEmptyComponent = ({filter}: {filter: Filter}) => { + const {search: assetSearchTerm, visible: isSearching} = useSearch() + const strings = useStrings() + const {styles} = useStyles() + + return ( + {filter === 'open' ? ( - }}> - } - > - - - + + + + + {isSearching ? `${strings.emptySearchOpenOrders} "${assetSearchTerm}"` : strings.emptyOpenOrders} + + + {!isSearching && {strings.emptyOpenOrdersSub}} + ) : ( - }}> - } - > - - - + + + + + {isSearching ? `${strings.emptySearchCompletedOrders} "${assetSearchTerm}"` : strings.emptyCompletedOrders} + + )} ) @@ -80,19 +267,87 @@ const useStyles = () => { const {color, atoms} = useTheme() const styles = StyleSheet.create({ + flex: { + ...atoms.flex_1, + }, group: { ...atoms.flex_row, ...atoms.gap_md, + ...atoms.justify_center, }, root: { ...atoms.flex_1, - ...atoms.justify_between, ...atoms.p_lg, + ...atoms.gap_lg, backgroundColor: color.bg_color_max, }, activeButton: { backgroundColor: color.el_gray_min, }, + list: { + ...atoms.gap_md, + }, + illustration: { + ...atoms.flex_1, + alignSelf: 'center', + width: 280, + height: 224, + }, + notOrdersYetContainer: { + ...atoms.flex_1, + ...atoms.text_center, + ...atoms.gap_lg, + ...atoms.pt_2xl, + }, + contentText: { + ...atoms.flex_1, + ...atoms.text_center, + ...atoms.heading_3_medium, + color: color.gray_max, + }, + contentSubText: { + ...atoms.flex_1, + ...atoms.text_center, + color: color.text_gray_low, + ...atoms.body_1_lg_regular, + }, + card: { + ...atoms.p_lg, + ...atoms.border, + borderRadius: 8, + borderColor: color.gray_200, + backgroundColor: color.bg_color_max, + }, + cardHeader: { + ...atoms.flex_row, + ...atoms.justify_between, + ...atoms.pb_md, + }, + composedText: { + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.gap_sm, + }, + heading: { + ...atoms.body_1_lg_medium, + color: color.text_gray_medium, + }, + row: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + rowLabel: { + ...atoms.body_1_lg_regular, + color: color.text_gray_low, + }, + rowValue: { + ...atoms.body_1_lg_regular, + color: color.text_gray_medium, + }, + inlineLink: { + padding: 0, + justifyContent: 'flex-end', + }, }) - return styles + return {styles, color} } diff --git a/packages/swap/src/adapters/api/muesliswap/api-maker.ts b/packages/swap/src/adapters/api/muesliswap/api-maker.ts index 13525c34da..2f28253e12 100644 --- a/packages/swap/src/adapters/api/muesliswap/api-maker.ts +++ b/packages/swap/src/adapters/api/muesliswap/api-maker.ts @@ -4,7 +4,6 @@ import {freeze} from 'immer' import { CancelRequest, CancelResponse, - OpenOrdersResponse, HistoryOrdersResponse, TokensResponse, CreateOrderResponse, @@ -24,7 +23,7 @@ export type MuesliswapApiConfig = { export const muesliswapApiMaker = ( config: MuesliswapApiConfig, ): Readonly => { - const {address, addressHex, network, request = fetchData} = config + const {address, network, request = fetchData} = config if (network !== Chain.Network.Mainnet) return new Proxy( @@ -77,49 +76,27 @@ export const muesliswapApiMaker = ( }, async orders() { - const [historyResponse, aggregatorResponse] = await Promise.all([ - request( - { - method: 'get', - url: apiUrls.orderHistory, - headers, - }, - { - params: { - user_address: address, - }, - }, - ), - request( - { - method: 'get', - url: apiUrls.openOrders, - headers, - }, - { - params: { - user_address: addressHex, - }, + const response = await request( + { + method: 'get', + url: apiUrls.orderHistory, + headers, + }, + { + params: { + user_address: address, }, - ), - ]) + }, + ) - if (isLeft(historyResponse)) return historyResponse - if (isLeft(aggregatorResponse)) return aggregatorResponse + if (isLeft(response)) return response return freeze( { tag: 'right', value: { status: 200, - data: [ - ...transformers.orderHistory.response( - historyResponse.value.data, - ), - ...transformers.openOrders.response( - aggregatorResponse.value.data, - ), - ], + data: transformers.orderHistory.response(response.value.data), }, }, true, diff --git a/yarn.lock b/yarn.lock index 170c8a9b15..485af97741 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15889,11 +15889,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"