-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8bcc843
commit b7dae66
Showing
350 changed files
with
18,908 additions
and
9,605 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* @uniswap/web-admins |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,24 @@ | ||
We are back with some new updates! Here’s the latest: | ||
IPFS hash of the deployment: | ||
- CIDv0: `QmYVT8bXNHTgPdSTegLQ62Z6f7E51fQbeMvn2nJAxMRGBE` | ||
- CIDv1: `bafybeiew2yawtry3lld6g5ankyq3bsbvjfchxxhboph45u6os6t2babtru` | ||
|
||
Token Warnings: See more information about the tokens you’re attempting to swap, enriched with data from Blockaid. | ||
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). | ||
|
||
You can also access the Uniswap Interface from an IPFS gateway. | ||
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. | ||
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org). | ||
Your Uniswap settings are never remembered across different URLs. | ||
|
||
IPFS gateways: | ||
- https://bafybeiew2yawtry3lld6g5ankyq3bsbvjfchxxhboph45u6os6t2babtru.ipfs.dweb.link/ | ||
- https://bafybeiew2yawtry3lld6g5ankyq3bsbvjfchxxhboph45u6os6t2babtru.ipfs.cf-ipfs.com/ | ||
- [ipfs://QmYVT8bXNHTgPdSTegLQ62Z6f7E51fQbeMvn2nJAxMRGBE/](ipfs://QmYVT8bXNHTgPdSTegLQ62Z6f7E51fQbeMvn2nJAxMRGBE/) | ||
|
||
### 5.62.4 (2024-12-16) | ||
|
||
|
||
### Bug Fixes | ||
|
||
* **web:** fix disabled swap button for previously-dismissed warning tokens (#14560) 2b04e71 | ||
|
||
Other changes: | ||
|
||
- Increased manual slippage tolerance up to 50% | ||
- Support for toggling between fiat and token input for fiat onramp | ||
- Better dapp signing support | ||
- Various bug fixes and performance improvements |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
mobile/1.41 | ||
web/5.62.4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
apps/extension/src/app/features/accounts/useSortedAccountList.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import { useSortedAccountList } from 'src/app/features/accounts/useSortedAccountList' | ||
import { act, renderHook } from 'src/test/test-utils' | ||
import { useAccountList } from 'wallet/src/features/accounts/hooks' | ||
|
||
jest.mock('wallet/src/features/accounts/hooks') | ||
const mockUseAccountList = useAccountList as jest.MockedFunction<typeof useAccountList> | ||
|
||
describe('useSortedAccountList', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks() | ||
}) | ||
|
||
it('should sort addresses by balance in descending order', () => { | ||
mockAccountList([mockPortfolio('address1', 100), mockPortfolio('address2', 200), mockPortfolio('address3', 150)]) | ||
|
||
const addresses = ['address1', 'address2', 'address3'] | ||
const { result } = renderHook(() => useSortedAccountList(addresses)) | ||
|
||
expect(result.current).toEqual([ | ||
{ address: 'address2', balance: 200 }, | ||
{ address: 'address3', balance: 150 }, | ||
{ address: 'address1', balance: 100 }, | ||
]) | ||
}) | ||
|
||
it('should handle undefined portfolios', () => { | ||
mockAccountList(undefined) | ||
|
||
const addresses = ['address1', 'address2'] | ||
const { result } = renderHook(() => useSortedAccountList(addresses)) | ||
|
||
expect(result.current).toEqual([ | ||
{ address: 'address1', balance: 0 }, | ||
{ address: 'address2', balance: 0 }, | ||
]) | ||
}) | ||
|
||
it('should use previous data during balance updates', () => { | ||
mockAccountList([mockPortfolio('address1', 100), mockPortfolio('address2', 200)]) | ||
|
||
const addresses = ['address1', 'address2'] | ||
const { result, rerender } = renderHook((props) => useSortedAccountList(props), { initialProps: addresses }) | ||
|
||
expect(result.current).toEqual([ | ||
{ address: 'address2', balance: 200 }, | ||
{ address: 'address1', balance: 100 }, | ||
]) | ||
|
||
mockAccountList([mockPortfolio('address1', 100)], true) | ||
rerender(['address1']) | ||
|
||
expect(result.current).toEqual([{ address: 'address1', balance: 100 }]) | ||
}) | ||
|
||
it('should keep list order when an account is removed', async () => { | ||
mockAccountList([mockPortfolio('address1', 100), mockPortfolio('address2', 200), mockPortfolio('address3', 300)]) | ||
|
||
const addresses = ['address1', 'address2', 'address3'] | ||
const { result, rerender } = renderHook((props) => useSortedAccountList(props), { initialProps: addresses }) | ||
|
||
expect(result.current).toEqual([ | ||
{ address: 'address3', balance: 300 }, | ||
{ address: 'address2', balance: 200 }, | ||
{ address: 'address1', balance: 100 }, | ||
]) | ||
|
||
mockAccountListUndefined() | ||
|
||
await act(async () => { | ||
rerender(['address1', 'address2']) | ||
await new Promise((resolve) => setTimeout(resolve, 100)) | ||
}) | ||
|
||
expect(result.current).toEqual([ | ||
{ address: 'address2', balance: 200 }, | ||
{ address: 'address1', balance: 100 }, | ||
]) | ||
|
||
mockAccountList([mockPortfolio('address1', 100), mockPortfolio('address2', 200)]) | ||
|
||
await act(async () => { | ||
rerender(['address1', 'address2']) | ||
}) | ||
|
||
expect(result.current).toEqual([ | ||
{ address: 'address2', balance: 200 }, | ||
{ address: 'address1', balance: 100 }, | ||
]) | ||
}) | ||
}) | ||
|
||
function mockPortfolio( | ||
ownerAddress: Address, | ||
balance: number, | ||
): { | ||
id: string | ||
ownerAddress: Address | ||
tokensTotalDenominatedValue: { __typename?: 'Amount'; value: number } | ||
} { | ||
return { | ||
id: ownerAddress, | ||
ownerAddress, | ||
tokensTotalDenominatedValue: { __typename: 'Amount', value: balance }, | ||
} | ||
} | ||
|
||
function mockAccountList(portfolios: ReturnType<typeof mockPortfolio>[] | undefined, loading = false): void { | ||
mockUseAccountList.mockReturnValue({ | ||
data: { portfolios }, | ||
loading, | ||
networkStatus: 7, | ||
refetch: jest.fn(), | ||
startPolling: jest.fn(), | ||
stopPolling: jest.fn(), | ||
}) | ||
} | ||
|
||
function mockAccountListUndefined(): void { | ||
mockUseAccountList.mockReturnValue({ | ||
data: undefined, | ||
loading: true, | ||
networkStatus: 7, | ||
refetch: jest.fn(), | ||
startPolling: jest.fn(), | ||
stopPolling: jest.fn(), | ||
}) | ||
} |
47 changes: 47 additions & 0 deletions
47
apps/extension/src/app/features/accounts/useSortedAccountList.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { useMemo } from 'react' | ||
import { usePrevious } from 'utilities/src/react/hooks' | ||
import { useAccountList } from 'wallet/src/features/accounts/hooks' | ||
|
||
interface AddressWithBalance { | ||
address: Address | ||
balance: number | ||
} | ||
|
||
export function useSortedAccountList(addresses: Address[]): AddressWithBalance[] { | ||
const { data: accountBalanceData } = useAccountList({ | ||
addresses, | ||
}) | ||
|
||
/* | ||
Why are we using previousAccountBalanceData? | ||
This is a workaround for a data fetching inefficiency. When removing an address, we send a new query | ||
with the updated address array, causing Apollo to refetch ALL balances. During this refetch, balances | ||
temporarily show as 0, causing the list to re-sort momentarily. | ||
We use previousAccountBalanceData to maintain the last known good balances during this refetch. The balances | ||
will be updated once the new query completes. | ||
*/ | ||
const previousAccountBalanceData = usePrevious(accountBalanceData) | ||
|
||
const balanceRecord: Record<Address, number> = useMemo(() => { | ||
const data = accountBalanceData || previousAccountBalanceData | ||
if (!data?.portfolios) { | ||
return {} | ||
} | ||
return Object.fromEntries( | ||
data.portfolios | ||
.filter((portfolio): portfolio is NonNullable<typeof portfolio> => Boolean(portfolio)) | ||
.map((portfolio) => [portfolio.ownerAddress, portfolio.tokensTotalDenominatedValue?.value ?? 0]), | ||
) | ||
}, [accountBalanceData, previousAccountBalanceData]) | ||
|
||
return useMemo(() => { | ||
return addresses | ||
.map((address) => ({ | ||
address, | ||
balance: balanceRecord[address] ?? 0, | ||
})) | ||
.sort((a, b) => b.balance - a.balance) | ||
}, [addresses, balanceRecord]) | ||
} |
114 changes: 114 additions & 0 deletions
114
apps/extension/src/app/features/appRating/AppRatingModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { useEffect, useState } from 'react' | ||
import { useDispatch } from 'react-redux' | ||
import { Button, Flex, Text, TouchableArea } from 'ui/src' | ||
import { Feedback, LikeSquare, MessageText, X } from 'ui/src/components/icons' | ||
import { IconSizeTokens, zIndices } from 'ui/src/theme' | ||
import { Modal } from 'uniswap/src/components/modals/Modal' | ||
import { uniswapUrls } from 'uniswap/src/constants/urls' | ||
import { ModalName } from 'uniswap/src/features/telemetry/constants' | ||
import { useTranslation } from 'uniswap/src/i18n' | ||
import { setAppRating } from 'wallet/src/features/wallet/slice' | ||
|
||
interface AppRatingModalProps { | ||
onClose: () => void | ||
} | ||
|
||
enum State { | ||
Initial, | ||
NotReally, | ||
Yes, | ||
} | ||
|
||
export default function AppRatingModal({ onClose }: AppRatingModalProps): JSX.Element | null { | ||
const { t } = useTranslation() | ||
const [state, setState] = useState(State.Initial) | ||
const dispatch = useDispatch() | ||
|
||
const stateConfig = { | ||
[State.Initial]: { | ||
title: t('appRating.extension.title'), | ||
description: t('appRating.description'), | ||
secondaryButtonText: t('appRating.button.notReally'), | ||
primaryButtonText: t('common.button.yes'), | ||
Icon: LikeSquare, | ||
iconSize: '$icon.24' as IconSizeTokens, | ||
onSecondaryButtonPress: () => setState(State.NotReally), | ||
onPrimaryButtonPress: () => setState(State.Yes), | ||
}, | ||
[State.NotReally]: { | ||
title: t('appRating.feedback.title'), | ||
description: t('appRating.feedback.description'), | ||
secondaryButtonText: t('common.button.notNow'), | ||
primaryButtonText: t('appRating.feedback.button.send'), | ||
Icon: MessageText, | ||
iconSize: '$icon.18' as IconSizeTokens, | ||
onSecondaryButtonPress: () => onClose(), | ||
onPrimaryButtonPress: (): void => { | ||
// eslint-disable-next-line security/detect-non-literal-fs-filename | ||
window.open(uniswapUrls.walletFeedbackForm) | ||
dispatch(setAppRating({ feedbackProvided: true })) | ||
onClose() | ||
}, | ||
}, | ||
[State.Yes]: { | ||
title: t('appRating.extension.review.title'), | ||
description: t('appRating.extension.review.description'), | ||
secondaryButtonText: t('common.button.notNow'), | ||
primaryButtonText: t('common.button.review'), | ||
Icon: Feedback, | ||
iconSize: '$icon.24' as IconSizeTokens, | ||
onSecondaryButtonPress: () => onClose(), | ||
onPrimaryButtonPress: (): void => { | ||
// eslint-disable-next-line security/detect-non-literal-fs-filename | ||
window.open(`https://chromewebstore.google.com/detail/uniswap-extension/${chrome.runtime.id}/reviews`) | ||
dispatch(setAppRating({ ratingProvided: true })) | ||
onClose() | ||
}, | ||
}, | ||
} | ||
|
||
const { | ||
title, | ||
description, | ||
secondaryButtonText, | ||
primaryButtonText, | ||
Icon, | ||
iconSize, | ||
onSecondaryButtonPress, | ||
onPrimaryButtonPress, | ||
} = stateConfig[state] | ||
|
||
useEffect(() => { | ||
// just to set that prompt has been shown | ||
dispatch(setAppRating({})) | ||
}, [dispatch]) | ||
|
||
return ( | ||
<Modal isDismissible isModalOpen name={ModalName.TokenWarningModal} backgroundColor="$surface1" onClose={onClose}> | ||
<TouchableArea p="$spacing16" position="absolute" right={0} top={0} zIndex={zIndices.default} onPress={onClose}> | ||
<X color="$neutral2" size="$icon.20" /> | ||
</TouchableArea> | ||
<Flex alignItems="center" gap="$spacing8" pt="$spacing16"> | ||
<Flex centered backgroundColor="$accent2" width="$spacing48" height="$spacing48" borderRadius="$rounded12"> | ||
<Icon color="$accent1" size={iconSize} /> | ||
</Flex> | ||
<Flex alignItems="center" gap="$spacing8" pb="$spacing16" pt="$spacing8" px="$spacing4"> | ||
<Text color="$neutral1" textAlign="center" variant="subheading2"> | ||
{title} | ||
</Text> | ||
<Text color="$neutral2" textAlign="center" variant="body3"> | ||
{description} | ||
</Text> | ||
</Flex> | ||
<Flex row width="100%" gap="$spacing12"> | ||
<Button flex={1} flexBasis={1} size="small" theme="secondary" onPress={onSecondaryButtonPress}> | ||
{secondaryButtonText} | ||
</Button> | ||
<Button flex={1} flexBasis={1} size="small" theme="primary" onPress={onPrimaryButtonPress}> | ||
{primaryButtonText} | ||
</Button> | ||
</Flex> | ||
</Flex> | ||
</Modal> | ||
) | ||
} |
Oops, something went wrong.