ci(release): publish latest release

parent e989b6d7
......@@ -30,6 +30,7 @@ types
apps/mobile/ios
apps/mobile/android
apps/mobile/.storybook/storybook.requires.ts
# extension
......@@ -42,3 +43,7 @@ packages/uniswap/codegen.ts
# eslint partials
packages/eslint-config/restrictedImports.js
# generated
packages/uniswap/src/data/rest/conversionTracking/api
* @uniswap/web-admins
We are back with some new updates! Here’s the latest:
IPFS hash of the deployment:
- CIDv0: `QmUKDwf73MabW8XFc1EBM7rPqxs9p7sCAPaznFgrLpSJ6S`
- CIDv1: `bafybeicyzcey52u465bnzqnoat6g6itt7tjmkc47mmssyzge2xif2iihc4`
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://bafybeicyzcey52u465bnzqnoat6g6itt7tjmkc47mmssyzge2xif2iihc4.ipfs.dweb.link/
- https://bafybeicyzcey52u465bnzqnoat6g6itt7tjmkc47mmssyzge2xif2iihc4.ipfs.cf-ipfs.com/
- [ipfs://QmUKDwf73MabW8XFc1EBM7rPqxs9p7sCAPaznFgrLpSJ6S/](ipfs://QmUKDwf73MabW8XFc1EBM7rPqxs9p7sCAPaznFgrLpSJ6S/)
## 5.62.0 (2024-12-11)
### Features
* **web:** add modification check to on chain tx (#14083) 964cb1e
* **web:** add network cost to create flow (#14224) 9e280d9
* **web:** add platform id tracking (#13780) 186a9e0
* **web:** only show token warning on swap button click for prefilled (#14118) 068dd89
* **web:** price range input tick tooltips (#14129) 3c66f56
* **web:** show usd values in current tick tooltip for price range input (#14157) 7ecd409
* **web:** wiring up conversion proxy service (#13935) 6759787
### Bug Fixes
* **web:** [3/n] remove react-spring (SwapLineItem) (#13914) 991557e
* **web:** android keyboard issue (#14262) 1042833
* **web:** block lvmh nfts (#14233) dbbb92d
* **web:** cherrypick of price inversion issue (#14479) 75092f6
* **web:** delay main API call until approvals are calculated (#14302) af8185b
* **web:** downgrade react-native-web to 0.19.10 (#14474) 206c46f
* **web:** enforce privacy opt out choices (#14486) f8c43ad
* **web:** fix broken link for providing lps (#14372) (#14412) 71a95cc
* **web:** fix crash with v2 pair where there is a pool but no liquidity (#14186) cd0fd54
* **web:** fix max button functionality on deposit form (#14285) 1de6b4b
* **web:** hide un-owned positions (#14448) 280514a
* **web:** improve x-y positioning of liquidity chart and brush in range input (#14128) e50620d
* **web:** landing page modal diet - part ii (#14409) d1bd48c
* **web:** landing page modal diet (#14349) 6547912
* **web:** move input token warning to post-swap button (#14077) 823bef9
* **web:** op usdc sends (#14189) 24d41ea
* **web:** prevent crash when sending on bnb chain (#14359) 496ab28
* **web:** prevent flash of old swap flow (#14335) de64de2
* **web:** price range input grab (#14127) b1046cf
* **web:** remove nondefault list tokens from common bases (#14187) 2c9db40
* **web:** remove react-spring from NFT pages (#13923) 6045f3c
* **web:** rename swap settings context to tx settings context (#14276) 87fa4f6
* **web:** resizing resets flow (#14232) d442d98
* **web:** skip trading api calls when the form is not filled out (#14245) 389413d
* **web:** update support articles for v4 and lp redesign (#14248) a109d09
* **web:** update the claim flow to use the saga to deal with switching chains (#14346) 23246e9
* **web:** update the create flow to get data from the sdk instead of … (#14427) 94999a8
* **web:** v4 blocking fixes (#14178) 873bce1
* **web:** wrap positions in multichain context (#14467) 1d9f25d
* **web:** zora zk v2 lp fix staging (#14485) 4490890
### Continuous Integration
* **web:** update sitemaps b5a5370
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
\ No newline at end of file
extension/1.11.0
\ No newline at end of file
web/5.62.0
\ No newline at end of file
......@@ -15,11 +15,11 @@
"@svgr/webpack": "8.0.1",
"@tamagui/core": "1.114.4",
"@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.39.0",
"@uniswap/analytics-events": "2.40.0",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18",
"@uniswap/universal-router-sdk": "4.5.2",
"@uniswap/v3-sdk": "3.18.1",
"@uniswap/v4-sdk": "1.10.3",
"@uniswap/universal-router-sdk": "4.7.0",
"@uniswap/v3-sdk": "3.19.0",
"@uniswap/v4-sdk": "1.12.0",
"dotenv-webpack": "8.0.1",
"ethers": "5.7.2",
"eventemitter3": "5.0.1",
......
......@@ -6,6 +6,7 @@ import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { AccountItem } from 'src/app/features/accounts/AccountItem'
import { CreateWalletModal } from 'src/app/features/accounts/CreateWalletModal'
import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal'
import { useSortedAccountList } from 'src/app/features/accounts/useSortedAccountList'
import { useDappContext } from 'src/app/features/dapp/DappContext'
import { updateDappConnectedAddressFromExtension } from 'src/app/features/dapp/actions'
import { useDappConnectedAccounts } from 'src/app/features/dapp/hooks'
......@@ -33,7 +34,6 @@ import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PlusCircle } from 'wallet/src/components/icons/PlusCircle'
import { MenuContent } from 'wallet/src/components/menu/MenuContent'
import { MenuContentItem } from 'wallet/src/components/menu/types'
import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga'
......@@ -148,17 +148,8 @@ export function AccountSwitcherScreen(): JSX.Element {
onPress: (): void => setShowRemoveWalletModal(true),
},
]
const { data: accountBalanceData } = useAccountList({
addresses: accountAddresses,
notifyOnNetworkStatusChange: true,
})
const sortedAddressesByBalance = accountAddresses
.map((address) => {
const wallet = accountBalanceData?.portfolios?.find((portfolio) => portfolio?.ownerAddress === address)
return { address, balance: wallet?.tokensTotalDenominatedValue?.value }
})
.sort((a, b) => (b.balance ?? 0) - (a.balance ?? 0))
const sortedAddressesByBalance = useSortedAccountList(accountAddresses)
const contentShadowProps = {
shadowColor: colors.shadowColor.val,
......
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(),
})
}
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])
}
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>
)
}
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { appRatingStateSelector } from 'wallet/src/features/appRating/selectors'
export const useAppRating = (): {
appRatingModalVisible: boolean
onAppRatingModalClose: () => void
} => {
const appRatingEnabled = useFeatureFlag(FeatureFlags.ExtensionAppRating)
const { shouldPrompt } = useSelector(appRatingStateSelector)
const [appRatingModalVisible, setAppRatingModalVisible] = useState(false)
useEffect(() => {
if (shouldPrompt && appRatingEnabled) {
setAppRatingModalVisible(true)
}
}, [appRatingEnabled, shouldPrompt])
const onAppRatingModalClose = (): void => {
setAppRatingModalVisible(false)
}
return {
appRatingModalVisible,
onAppRatingModalClose,
}
}
......@@ -14,9 +14,7 @@ import { SignTypedDataRequestContent } from 'src/app/features/dappRequests/reque
import { rejectAllRequests } from 'src/app/features/dappRequests/saga'
import { isDappRequestStoreItemForEthSendTxn } from 'src/app/features/dappRequests/slice'
import {
isGetAccountRequest,
isRequestAccountRequest,
isRequestPermissionsRequest,
isConnectionRequest,
isSignMessageRequest,
isSignTypedDataRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
......@@ -201,11 +199,7 @@ const DappRequest = memo(function _DappRequest(): JSX.Element | null {
if (isDappRequestStoreItemForEthSendTxn(request)) {
return <EthSendRequestContent request={request} />
}
if (
isGetAccountRequest(request.dappRequest) ||
isRequestAccountRequest(request.dappRequest) ||
isRequestPermissionsRequest(request.dappRequest)
) {
if (isConnectionRequest(request.dappRequest)) {
return <ConnectionRequestContent />
}
......
......@@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { DappInfo } from 'src/app/features/dapp/store'
import {
DappRequest,
isConnectionRequest,
isSendTransactionRequest,
SendTransactionRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
......@@ -42,6 +43,14 @@ const slice = createSlice({
initialState: initialDappRequestState,
reducers: {
add: (state, action: PayloadAction<DappRequestStoreItem>) => {
if (isConnectionRequest(action.payload.dappRequest)) {
// Remove existing connection requests from the same tab and of the same type
state.pending = state.pending.filter(
(request) =>
request.senderTabInfo.id !== action.payload.senderTabInfo.id ||
request.dappRequest.type !== action.payload.dappRequest.type,
)
}
state.pending.push(action.payload)
},
remove: (state, action: PayloadAction<string>) => {
......
......@@ -384,3 +384,12 @@ export function isRequestAccountRequest(request: DappRequest): request is Reques
export function isRequestPermissionsRequest(request: DappRequest): request is RequestPermissionsRequest {
return RequestPermissionsRequestSchema.safeParse(request).success
}
export function isConnectionRequest(request: DappRequest): boolean {
return (
isGetAccountRequest(request) ||
isRequestAccountRequest(request) ||
isRequestPermissionsRequest(request)
)
}
......@@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { ActivityTab } from 'src/app/components/tabs/ActivityTab'
import { NftsTab } from 'src/app/components/tabs/NftsTab'
import AppRatingModal from 'src/app/features/appRating/AppRatingModal'
import { useAppRating } from 'src/app/features/appRating/hooks/useAppRating'
import { PortfolioActionButtons } from 'src/app/features/home/PortfolioActionButtons'
import { PortfolioHeader } from 'src/app/features/home/PortfolioHeader'
import { TokenBalanceList } from 'src/app/features/home/TokenBalanceList'
......@@ -111,6 +113,8 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element {
}
}, [apolloClient, shouldRefetchNfts])
const { appRatingModalVisible, onAppRatingModalClose } = useAppRating()
return (
<Flex fill alignItems="center" backgroundColor="$surface1" p="$spacing12">
{address ? (
......@@ -193,6 +197,7 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element {
{t('home.extension.error')}
</Text>
)}
{appRatingModalVisible && <AppRatingModal onClose={onAppRatingModalClose} />}
</Flex>
)
})
......
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.11.0",
"version": "1.12.0",
"minimum_chrome_version": "116",
"icons": {
"16": "assets/icon16.png",
......
module.exports = {
root: true,
extends: ['@uniswap/eslint-config/native'],
ignorePatterns: ['.storybook/storybook.requires.ts'],
parserOptions: {
project: 'tsconfig.eslint.json',
tsconfigRootDir: __dirname,
......
const StorybookUIRoot = () => null
export default StorybookUIRoot
import { MMKV } from 'react-native-mmkv'
import { view } from './storybook.requires'
const mmkv = new MMKV({
id: 'storybook-wallet',
})
const StorybookUIRoot = view.getStorybookUI({
storage: {
getItem: (key): Promise<string | null> => Promise.resolve(mmkv.getString(key) || null),
setItem: (key, value): Promise<void> => {
mmkv.set(key, value)
return Promise.resolve()
},
},
})
export default StorybookUIRoot
import { StorybookConfig } from '@storybook/react-native'
const main: StorybookConfig = {
stories: ['../src/**/*.stories.?(ts|tsx|js|jsx)', '../../../packages/ui/src/**/*.stories.?(ts|tsx|js|jsx)'],
addons: ['@storybook/addon-ondevice-controls'],
}
export default main
import type { Preview } from '@storybook/react'
const preview: Preview = {}
export default preview
/* do not change this file, it is auto generated by storybook. */
import { start, updateView } from "@storybook/react-native";
import "@storybook/addon-ondevice-controls/register";
const normalizedStories = [
{
titlePrefix: "",
directory: "./src",
files: "**/*.stories.?(ts|tsx|js|jsx)",
importPathMatcher:
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/,
// @ts-ignore
req: require.context(
"../src",
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/
),
},
{
titlePrefix: "",
directory: "../../packages/ui/src",
files: "**/*.stories.?(ts|tsx|js|jsx)",
importPathMatcher:
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/,
// @ts-ignore
req: require.context(
"../../../packages/ui/src",
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/
),
},
];
declare global {
var view: ReturnType<typeof start>;
var STORIES: typeof normalizedStories;
}
const annotations = [
require("./preview"),
require("@storybook/react-native/preview"),
];
global.STORIES = normalizedStories;
// @ts-ignore
module?.hot?.accept?.();
if (!global.view) {
global.view = start({
annotations,
storyEntries: normalizedStories,
});
} else {
updateView(global.view, annotations, normalizedStories);
}
export const view = global.view;
......@@ -230,7 +230,7 @@ To get started, you should already be able to build the iOS app (either in XCode
One you have a device configured, it will start to build. If/when successful, you'll see the device simulator/emulator in the sidebar.
In `.vscode/launch.json`, you will see configurations for each platform. This is where you can specify the fingerprint command. The fingerprint is a hash of the build environment, and Radon uses it to determine if the build has changed so that it knows when to re-run the build process (i.e. only on native code changes). There are more complex implementations of this, but this is a simple first step.
In `.vscode/launch.json`, you will see configurations for each platform. This is where you can specify the fingerprint command. The fingerprint is a hash of the build environment, and Radon uses it to determine if the build has changed so that it knows when to re-run the build process (i.e. only on native code changes). See `getFingerprintForRadonIDE.js` for more details. There are more complex implementations of this, but this is a simple first step.
#### Running on a Physical iOS Device
......@@ -240,17 +240,6 @@ In `.vscode/launch.json`, you will see configurations for each platform. This is
4. Select the Uniswap target + your connect device, then `Cmd + R` or use the ▶️ button the start the build
5. You may get an error about your device not yet being added to the Uniswap Apple Developer account; if so, click `Register` and restart the build
### Enabling Flipper
We do not check Flipper into source. To prevent `pod install` from adding Flipper, set an environment variable in your `.bash_profile` or `.zshrc` or `.zprofile`:
```bash
# To enable flipper inclusion (optional)
export USE_FLIPPER=1
```
Note: To disable Flipper, the whole line should be commented out, as setting this value to 0 will not disable Flipper.
## Important Libraries and Tools
These are some tools you might want to familiarize yourself with to understand the codebase better and how different aspects of it work.
......@@ -324,8 +313,3 @@ eval "$(/opt/homebrew/bin/brew shellenv)"
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh" # This loads nvm
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" # This loads nvm bash_completion
# To enable flipper inclusion (optional)
export USE_FLIPPER=1
export PATH="/opt/homebrew/opt/ruby/bin:$PATH"
```
import AsyncStorage from '@react-native-async-storage/async-storage'
import type { ReactotronReactNative } from 'reactotron-react-native'
import Reactotron, { openInEditor } from 'reactotron-react-native'
import mmkvPlugin from 'reactotron-react-native-mmkv'
import { reactotronRedux } from 'reactotron-redux'
import { MMKV } from 'react-native-mmkv'
const storage = new MMKV()
const reactotron = Reactotron.setAsyncStorageHandler(AsyncStorage)
.configure({
name: 'Uniswap Wallet',
onConnect: () => {
Reactotron.clear()
},
})
.use(mmkvPlugin<ReactotronReactNative>({ storage, ignore: ['react-query-cache', 'apollo-cache-persist'] }))
.use(reactotronRedux())
.use(openInEditor())
.useReactNative()
.connect()
export default reactotron
......@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
}
def devVersionName = "1.41"
def betaVersionName = "1.41"
def prodVersionName = "1.41"
def devVersionName = "1.42"
def betaVersionName = "1.42"
def prodVersionName = "1.42"
android {
ndkVersion rootProject.ext.ndkVersion
......@@ -104,7 +104,6 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
......@@ -209,9 +208,6 @@ dependencies {
implementation "com.facebook.react:react-android"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
// Used to deal with Flipper/OkHttp issues with DevSupportManager
implementation "androidx.multidex:multidex:$multidexVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerialization"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle"
......@@ -255,8 +251,6 @@ dependencies {
implementation("androidx.core:core-performance:$corePerf")
implementation("androidx.core:core-performance-play-services:$corePerf")
implementation("com.facebook.react:flipper-integration")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
......
package com.uniswap
import android.app.Application
import android.content.res.Configuration
import androidx.multidex.MultiDexApplication
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader
import com.shopify.reactnativeperformance.ReactNativePerformance
import com.uniswap.onboarding.scantastic.ScantasticEncryptionModule
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
import com.uniswap.RedirectToSourceAppPackage
class MainApplication : MultiDexApplication(), ReactApplication {
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
......@@ -57,7 +54,6 @@ class MainApplication : MultiDexApplication(), ReactApplication {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager);
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
......
......@@ -23,7 +23,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.uniswap.onboarding.shared.CopyButton
import com.uniswap.theme.UniswapTheme
import com.uniswap.theme.relativeOffset
import kotlin.math.abs
......@@ -73,7 +72,8 @@ fun MnemonicDisplay(
CopyButton(
copyButtonText = copyText,
copiedButtonText = copiedText,
textToCopy = textToCopy
textToCopy = textToCopy,
isSensitive = true
)
}
}
......
package com.uniswap.onboarding.shared
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.os.PersistableBundle
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.border
......@@ -12,6 +18,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
......@@ -22,12 +29,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.uniswap.R
import com.uniswap.theme.UniswapTheme
import kotlinx.coroutines.delay
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
enum class ActionButtonStatus {
SUCCESS, NEUTRAL
......@@ -88,14 +98,37 @@ fun ActionButton(
}
}
/**
* A composable function that displays a button for copying text to the clipboard. On click
* it shows that the text has been copied and will allow copying again after a delay. Optionally
* if the text is sensitive, it will be marked as sensitive and cleared from the clipboard after
* a delay.
*
* @param copyButtonText The text to display on the button when the text has not been copied yet.
* @param copiedButtonText The text to display on the button after the text has been copied.
* @param textToCopy The [AnnotatedString] that will be copied to the clipboard when the button is clicked.
* @param isSensitive An optional parameter specifying that the text being copied is sensitive and should
* be marked as so. Additionally it will be cleared after time.
*/
@Composable
fun CopyButton(
modifier: Modifier = Modifier,
copyButtonText: String,
copiedButtonText: String,
textToCopy: AnnotatedString
textToCopy: AnnotatedString,
isSensitive: Boolean = false,
) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val backgroundExecutor = remember {
Executors.newSingleThreadScheduledExecutor()
}
DisposableEffect(Unit) {
onDispose {
backgroundExecutor.shutdown()
}
}
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
// Time after which the button reverts to the copy state
val copyTimeout: Long = 2000
......@@ -103,11 +136,28 @@ fun CopyButton(
var copyTimeoutId by remember { mutableStateOf(0) }
fun onClick() {
clipboardManager.setText(textToCopy)
val label = if (isSensitive) "sensitive data" else "copied text"
val clipData = ClipData.newPlainText(label, textToCopy)
if (isSensitive) {
clipData.description.extras = PersistableBundle().apply {
val extraKey = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ClipDescription.EXTRA_IS_SENSITIVE
} else {
"android.content.extra.IS_SENSITIVE"
}
putBoolean(extraKey, true)
}
backgroundExecutor.schedule({
clipboard.clearPrimaryClip()
}, 2, TimeUnit.MINUTES)
}
clipboard.setPrimaryClip(clipData)
copyTimeoutId++
isCopied = true
}
LaunchedEffect(copyTimeoutId) {
delay(copyTimeout)
isCopied = false
......
......@@ -15,7 +15,6 @@ buildscript {
flowlayout = "0.23.1"
kotlinSerialization = "1.5.1"
lifecycle = "2.5.1"
multidexVersion = "2.0.1"
}
repositories {
google()
......
......@@ -26,9 +26,6 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.212.0
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
// Disable sorting imports with Prettier for this file so that it doesn't change the order
// organize-imports-ignore
import './wdyr'
import { isNonJestDev } from 'utilities/src/environment/constants'
if (isNonJestDev) {
require('./ReactotronConfig')
}
import { AppRegistry } from 'react-native'
import 'react-native-gesture-handler'
......
......@@ -28,13 +28,10 @@ target 'Uniswap' do
use_expo_modules!(exclude: ['expo-constants','expo-file-system', 'expo-font', 'expo-keep-awake', 'expo-error-recovery'])
config = use_native_modules!
flipper_config = ENV['USE_FLIPPER'] ? FlipperConfiguration.enabled : FlipperConfiguration.disabled
use_react_native!(
:path => config[:reactNativePath],
# to enable hermes on iOS, change `false` to `true` and then install pods
:hermes_enabled => true,
:flipper_configuration => flipper_config
:hermes_enabled => true
)
target 'UniswapTests' do
......
......@@ -2364,6 +2364,10 @@ PODS:
- React
- React-callinvoker
- React-Core
- react-native-slider (4.5.5):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- react-native-webview (11.23.1):
- React-Core
- react-native-widgetkit (1.0.9):
......@@ -2542,6 +2546,8 @@ PODS:
- React-Core
- RNCMaskedView (0.2.9):
- React-Core
- RNDateTimePicker (8.2.0):
- React-Core
- RNDeviceInfo (10.0.2):
- React-Core
- RNFastImage (8.6.3):
......@@ -2682,6 +2688,7 @@ DEPENDENCIES:
- react-native-restart (from `../../../node_modules/react-native-restart`)
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
- "react-native-skia (from `../../../node_modules/@shopify/react-native-skia`)"
- "react-native-slider (from `../../../node_modules/@react-native-community/slider`)"
- react-native-webview (from `../../../node_modules/react-native-webview`)
- react-native-widgetkit (from `../../../node_modules/react-native-widgetkit`)
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
......@@ -2707,6 +2714,7 @@ DEPENDENCIES:
- "ReactNativePerformance (from `../../../node_modules/@shopify/react-native-performance`)"
- "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
- "RNCMaskedView (from `../../../node_modules/@react-native-masked-view/masked-view`)"
- "RNDateTimePicker (from `../../../node_modules/@react-native-community/datetimepicker`)"
- RNDeviceInfo (from `../../../node_modules/react-native-device-info`)
- RNFastImage (from `../../../node_modules/react-native-fast-image`)
- "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)"
......@@ -2895,6 +2903,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native-safe-area-context"
react-native-skia:
:path: "../../../node_modules/@shopify/react-native-skia"
react-native-slider:
:path: "../../../node_modules/@react-native-community/slider"
react-native-webview:
:path: "../../../node_modules/react-native-webview"
react-native-widgetkit:
......@@ -2945,6 +2955,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/@react-native-async-storage/async-storage"
RNCMaskedView:
:path: "../../../node_modules/@react-native-masked-view/masked-view"
RNDateTimePicker:
:path: "../../../node_modules/@react-native-community/datetimepicker"
RNDeviceInfo:
:path: "../../../node_modules/react-native-device-info"
RNFastImage:
......@@ -3078,6 +3090,7 @@ SPEC CHECKSUMS:
react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162
react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b
react-native-skia: c7d8d8a380d4dbf8463343e2738bceaf49a9c084
react-native-slider: 708dea8f20c91a325d622fe8e11d3b9dfff158a3
react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581
react-native-widgetkit: efb6680df237463bbe1be3a4d1a1578a1b0bb08f
React-nativeconfig: e700ac3ec3d66329076bd2c2787a204411815d43
......@@ -3104,6 +3117,7 @@ SPEC CHECKSUMS:
RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21
RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca
RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6
RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14
RNDeviceInfo: 0a7c1d2532aa7691f9b9925a27e43af006db4dae
RNFastImage: 246de6b52d7642992cfd01e2005dda36d00a6660
RNFBApp: 4122dd41d8d7ff017b6ecf777a6224f5b349ca04
......@@ -3126,6 +3140,6 @@ SPEC CHECKSUMS:
Yoga: 805bf71192903b20fc14babe48080582fee65a80
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: e4219214e71cc0c904d669166daa324a8e54be8f
PODFILE CHECKSUM: 525fd4a1c78879023ae05970b18e66b654c4c07a
COCOAPODS: 1.14.3
......@@ -164,7 +164,6 @@
8EE7C0582AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE7C0572AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift */; };
9127D1362CC2D3D00096F134 /* TokenBalanceMainParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9127D1342CC2D3D00096F134 /* TokenBalanceMainParts.graphql.swift */; };
9127D1372CC2D3D00096F134 /* TokenBalanceQuantityParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9127D1352CC2D3D00096F134 /* TokenBalanceQuantityParts.graphql.swift */; };
914CBAA274C1F15A53C35CB6 /* libPods-WidgetsCoreTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B2176D5449C2B3B68A17466B /* libPods-WidgetsCoreTests.a */; };
91D501702CDBEAE700B09B7F /* TokenMarketParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D501652CDBEAE700B09B7F /* TokenMarketParts.graphql.swift */; };
91D501712CDBEAE700B09B7F /* TokenBalanceMainParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D501662CDBEAE700B09B7F /* TokenBalanceMainParts.graphql.swift */; };
91D501722CDBEAE700B09B7F /* TokenProjectMarketsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D501672CDBEAE700B09B7F /* TokenProjectMarketsParts.graphql.swift */; };
......@@ -2205,7 +2204,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2258,7 +2257,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2311,7 +2310,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2364,7 +2363,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2402,7 +2401,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2438,7 +2437,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2473,7 +2472,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2508,7 +2507,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2555,7 +2554,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2601,7 +2600,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
......@@ -2647,7 +2646,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
......@@ -2693,7 +2692,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
......@@ -2735,7 +2734,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2778,7 +2777,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
......@@ -2821,7 +2820,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
......@@ -2864,7 +2863,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
......@@ -2900,7 +2899,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -2938,7 +2937,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3138,7 +3137,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -3182,7 +3181,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
......@@ -3293,7 +3292,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3364,7 +3363,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
......@@ -3475,7 +3474,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3546,7 +3545,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.41;
MARKETING_VERSION = 1.42;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
......@@ -7,8 +7,10 @@
const { getMetroAndroidAssetsResolutionFix } = require('react-native-monorepo-tools')
const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix()
const withStorybook = require('@storybook/react-native/metro/withStorybook')
const path = require('path')
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')
const mobileRoot = path.resolve(__dirname)
const workspaceRoot = path.resolve(mobileRoot, '../..')
......@@ -17,18 +19,18 @@ const watchFolders = [mobileRoot, `${workspaceRoot}/node_modules`, `${workspaceR
const detoxExtensions = process.env.DETOX_MODE === 'mocked' ? ['mock.tsx', 'mock.ts'] : []
const defaultConfig = getDefaultConfig(__dirname);
const defaultConfig = getDefaultConfig(__dirname)
const {
resolver: { sourceExts, assetExts },
} = defaultConfig;
} = defaultConfig
const config = {
resolver: {
nodeModulesPaths: [`${workspaceRoot}/node_modules`],
assetExts: assetExts.filter((ext) => ext !== 'svg'),
// detox mocking works properly only being spreaded at the beginning of sourceExts array
sourceExts: [...detoxExtensions, ...sourceExts, 'svg', 'cjs']
sourceExts: [...detoxExtensions, ...sourceExts, 'svg', 'cjs'],
},
transformer: {
getTransformOptions: async () => ({
......@@ -48,4 +50,11 @@ const config = {
watchFolders,
}
module.exports = mergeConfig(defaultConfig, config);
// Checkout more useful options in the docs: https://github.com/storybookjs/react-native?tab=readme-ov-file#options
module.exports = withStorybook(mergeConfig(defaultConfig, config), {
// Set to false to remove storybook specific options
// you can also use a env variable to set this
enabled: process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',
// Path to your storybook config
configPath: path.resolve(__dirname, './.storybook'),
})
......@@ -13,6 +13,7 @@
"check:deps:usage": "./scripts/checkDepsUsage.sh",
"clean": "react-native-clean-project",
"debug": "react-devtools",
"debug:reactotron:install": "./scripts/installDebugger.sh",
"deduplicate": "yarn-deduplicate --strategy=fewer",
"depcheck": "depcheck",
"env:android:keystore:download": "bash ./scripts/downloadAndroidKeystore.sh",
......@@ -32,8 +33,7 @@
"firestore:deploy:rules": "firebase deploy --only firestore:rules",
"link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
"hardhat": "hardhat node",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 10",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 8",
"ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios",
"ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift",
"ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"",
......@@ -50,7 +50,9 @@
"snapshots": "jest -u",
"typecheck": "tsc -b",
"unicons": "cd scripts && python3 populate_svgs.py && cd .. && yarn lint --fix",
"pod": "./scripts/podinstall.sh"
"pod": "./scripts/podinstall.sh",
"storybook:generate": "sb-rn-get-stories",
"prestart": "yarn storybook:generate"
},
"dependencies": {
"@amplitude/analytics-react-native": "1.4.0",
......@@ -60,6 +62,7 @@
"@ethersproject/shims": "5.6.0",
"@formatjs/intl-datetimeformat": "4.5.1",
"@formatjs/intl-getcanonicallocales": "1.9.0",
"@formatjs/intl-listformat": "7.7.5",
"@formatjs/intl-locale": "2.4.44",
"@formatjs/intl-numberformat": "7.4.1",
"@formatjs/intl-pluralrules": "4.3.1",
......@@ -85,10 +88,10 @@
"@shopify/react-native-skia": "1.4.2",
"@sparkfabrik/react-native-idfa-aaid": "1.2.0",
"@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.39.0",
"@uniswap/analytics-events": "2.40.0",
"@uniswap/client-explore": "0.0.12",
"@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "5.9.0",
"@uniswap/sdk-core": "6.0.0",
"@walletconnect/core": "2.17.1",
"@walletconnect/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1",
......@@ -162,7 +165,11 @@
"@babel/runtime": "7.18.9",
"@datadog/datadog-ci": "2.39.0",
"@faker-js/faker": "7.6.0",
"@storybook/react": "7.0.2",
"@react-native-community/datetimepicker": "8.2.0",
"@react-native-community/slider": "4.5.5",
"@storybook/addon-ondevice-controls": "8.4.2",
"@storybook/react": "8.4.2",
"@storybook/react-native": "8.4.2",
"@tamagui/babel-plugin": "1.114.4",
"@testing-library/react-native": "11.5.0",
"@types/redux-mock-store": "1.0.6",
......@@ -176,7 +183,6 @@
"detox": "20.23.0",
"eslint": "8.44.0",
"expo-modules-core": "1.11.13",
"hardhat": "2.14.0",
"jest": "29.7.0",
"jest-expo": "50.0.4",
"jest-extended": "4.0.1",
......@@ -189,11 +195,12 @@
"react-native-asset": "2.1.1",
"react-native-clean-project": "4.0.1",
"react-native-dotenv": "3.2.0",
"react-native-flipper": "0.212.0",
"react-native-monorepo-tools": "1.2.1",
"react-native-svg-transformer": "1.3.0",
"react-test-renderer": "18.2.0",
"redux-flipper": "2.0.2",
"reactotron-react-native": "5.1.10",
"reactotron-react-native-mmkv": "0.2.7",
"reactotron-redux": "3.1.10",
"redux-saga-test-plan": "4.0.4",
"typescript": "5.3.3",
"yarn-deduplicate": "6.0.0"
......
module.exports = {
assets: ['./src/assets/fonts'],
dependencies: {
...(process.env.USE_FLIPPER ? {} : { 'react-native-flipper': { platforms: { ios: null } } }),
},
}
// This file is run by Radon IDE to get the fingerprint for the current build
// Change the string to update the fingerprint for the current build, forcing Radon to re-run the build process
// Usually, this should only be necessary when there are native code changes relative to the most recent RadonIDE build
/* This file is run by Radon IDE to get the fingerprint for the current build
Change the string to update the fingerprint for the current build, forcing Radon to re-run the build process
Usually, this should only be necessary when there are native code changes relative to the most recent RadonIDE build
Avoid committing changes to this file! */
console.log('2024-11-13')
#!/bin/bash
which -s brew
if [[ $? != 0 ]] ; then
echo 'Homebrew has not been found to install Reactotron! Installing Homebrew now.'
# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
else
brew update
fi
brew install --cask reactotron
import { ApolloProvider } from '@apollo/client'
import {
DatadogProvider,
DatadogProviderConfiguration,
DdRum,
DdSdkReactNative,
SdkVerbosity,
} from '@datadog/mobile-react-native'
import { DdRum, DdSdkReactNative } from '@datadog/mobile-react-native'
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance'
import { MMKVWrapper } from 'apollo3-cache-persist'
import * as SplashScreen from 'expo-splash-screen'
import { PropsWithChildren, default as React, StrictMode, useCallback, useEffect } from 'react'
import { default as React, StrictMode, useCallback, useEffect } from 'react'
import { I18nextProvider } from 'react-i18next'
import { LogBox, NativeModules, StatusBar } from 'react-native'
import appsFlyer from 'react-native-appsflyer'
......@@ -21,6 +15,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens'
import { useDispatch, useSelector } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import { DatadogProviderWrapper } from 'src/app/DataDogProvider'
import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider'
import { AppModals } from 'src/app/modals/AppModals'
import { NavigationContainer } from 'src/app/navigation/NavigationContainer'
......@@ -45,10 +40,9 @@ import {
setI18NUserDefaults,
} from 'src/features/widgets/widgets'
import { useAppStateTrigger } from 'src/utils/useAppStateTrigger'
import { getDatadogEnvironment, getStatsigEnvironmentTier } from 'src/utils/version'
import { getStatsigEnvironmentTier } from 'src/utils/version'
import { flexStyles, useIsDarkMode } from 'ui/src'
import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner'
import { config } from 'uniswap/src/config'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors'
......@@ -70,7 +64,7 @@ import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/conte
import i18n from 'uniswap/src/i18n/i18n'
import { CurrencyId } from 'uniswap/src/types/currency'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { isDetoxBuild, isJestRun } from 'utilities/src/environment/constants'
import { isDetoxBuild } from 'utilities/src/environment/constants'
import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/Datadog'
import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { logger } from 'utilities/src/logger/logger'
......@@ -99,29 +93,6 @@ if (__DEV__) {
// Keep the splash screen visible while we fetch resources until one of our landing pages loads
SplashScreen.preventAutoHideAsync().catch(() => undefined)
// Datadog
const datadogConfig = new DatadogProviderConfiguration(
config.datadogClientToken,
getDatadogEnvironment(),
config.datadogProjectId,
!__DEV__, // trackInteractions
!__DEV__, // trackResources
!__DEV__, // trackErrors
)
datadogConfig.site = 'US1'
datadogConfig.longTaskThresholdMs = 100
datadogConfig.nativeCrashReportEnabled = true
datadogConfig.verbosity = SdkVerbosity.INFO
// Datadog does not expose event type, hence we can not type return
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
datadogConfig.errorEventMapper = (event) => {
// this is Sentry error, which is caused by the not complete closing of their SDK
if (event.message.includes('Native is disabled')) {
return null
}
return event
}
// Log boxes on simulators can block detox tap event when they cover buttons placed at
// the bottom of the screen and cause tests to fail.
if (isDetoxBuild) {
......@@ -200,15 +171,6 @@ function App(): JSX.Element | null {
)
}
function DatadogProviderWrapper({ children }: PropsWithChildren): JSX.Element {
logger.setWalletDatadogEnabled(true)
if (isDetoxBuild || isJestRun) {
return <>{children}</>
}
return <DatadogProvider configuration={datadogConfig}>{children}</DatadogProvider>
}
const MAX_CACHE_SIZE_IN_BYTES = 1024 * 1024 * 25 // 25 MB
// Ensures redux state is available inside usePersistedApolloClient for the custom endpoint
......
import {
BatchSize,
DatadogProvider,
DatadogProviderConfiguration,
SdkVerbosity,
TrackingConsent,
UploadFrequency,
} from '@datadog/mobile-react-native'
import { ErrorEventMapper } from '@datadog/mobile-react-native/lib/typescript/rum/eventMappers/errorEventMapper'
import { PropsWithChildren, default as React } from 'react'
import { getDatadogEnvironment } from 'src/utils/version'
import { config } from 'uniswap/src/config'
import { isDetoxBuild, isJestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger'
const ENABLE_DATADOG = localDevDatadogEnabled || !__DEV__
const datadogConfig = new DatadogProviderConfiguration(
config.datadogClientToken,
getDatadogEnvironment(),
config.datadogProjectId,
ENABLE_DATADOG, // trackInteractions
ENABLE_DATADOG, // trackResources
ENABLE_DATADOG, // trackErrors
localDevDatadogEnabled ? TrackingConsent.GRANTED : undefined,
)
Object.assign(datadogConfig, {
site: 'US1',
longTaskThresholdMs: 100,
nativeCrashReportEnabled: true,
verbosity: SdkVerbosity.INFO,
errorEventMapper: (event: ReturnType<ErrorEventMapper>) => {
// this is Sentry error, which is caused by the not complete closing of their SDK
if (event?.message.includes('Native is disabled')) {
return null
}
return event
},
})
if (localDevDatadogEnabled) {
Object.assign(datadogConfig, {
sessionSamplingRate: 100,
resourceTracingSamplingRate: 100,
uploadFrequency: UploadFrequency.FREQUENT,
batchSize: BatchSize.SMALL,
verbosity: SdkVerbosity.DEBUG,
})
}
/**
* Wrapper component to provide Datadog to the app with our mobile app's
* specific configuration.
*/
export function DatadogProviderWrapper({ children }: PropsWithChildren): JSX.Element {
logger.setWalletDatadogEnabled(true)
if (isDetoxBuild || isJestRun) {
return <>{children}</>
}
return <DatadogProvider configuration={datadogConfig}>{children}</DatadogProvider>
}
import {
BatchSize,
DatadogProvider,
DatadogProviderConfiguration,
SdkVerbosity,
TrackingConsent,
UploadFrequency,
} from '@datadog/mobile-react-native'
import { ErrorEventMapper } from '@datadog/mobile-react-native/lib/typescript/rum/eventMappers/errorEventMapper'
import { PropsWithChildren, default as React } from 'react'
import { getDatadogEnvironment } from 'src/utils/version'
import { config } from 'uniswap/src/config'
import { isDetoxBuild, isJestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger'
const ENABLE_DATADOG = localDevDatadogEnabled || !__DEV__
const datadogConfig = new DatadogProviderConfiguration(
config.datadogClientToken,
getDatadogEnvironment(),
config.datadogProjectId,
ENABLE_DATADOG, // trackInteractions
ENABLE_DATADOG, // trackResources
ENABLE_DATADOG, // trackErrors
localDevDatadogEnabled ? TrackingConsent.GRANTED : undefined,
)
Object.assign(datadogConfig, {
site: 'US1',
longTaskThresholdMs: 100,
nativeCrashReportEnabled: true,
verbosity: SdkVerbosity.INFO,
errorEventMapper: (event: ReturnType<ErrorEventMapper>) => {
// this is Sentry error, which is caused by the not complete closing of their SDK
if (event?.message.includes('Native is disabled')) {
return null
}
return event
},
})
if (localDevDatadogEnabled) {
Object.assign(datadogConfig, {
sessionSamplingRate: 100,
resourceTracingSamplingRate: 100,
uploadFrequency: UploadFrequency.FREQUENT,
batchSize: BatchSize.SMALL,
verbosity: SdkVerbosity.DEBUG,
})
}
/**
* Wrapper component to provide Datadog to the app with our mobile app's
* specific configuration.
*/
export function DatadogProviderWrapper({ children }: PropsWithChildren): JSX.Element {
logger.setWalletDatadogEnabled(true)
if (isDetoxBuild || isJestRun) {
return <>{children}</>
}
return <DatadogProvider configuration={datadogConfig}>{children}</DatadogProvider>
}
import { createNavigationContainerRef, NavigationContainer } from '@react-navigation/native'
import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { createStackNavigator, TransitionPresets } from '@react-navigation/stack'
import React from 'react'
import { TransitionPresets, createStackNavigator } from '@react-navigation/stack'
import React, { useEffect } from 'react'
import { DevSettings } from 'react-native'
import { useSelector } from 'react-redux'
import StorybookUIRoot from 'src/../.storybook'
import { navigationRef } from 'src/app/navigation/NavigationContainer'
import { renderHeaderBackButton, renderHeaderBackImage } from 'src/app/navigation/components'
import {
AppStackParamList,
......@@ -11,6 +14,7 @@ import {
FiatOnRampStackParamList,
OnboardingStackParamList,
SettingsStackParamList,
useAppStackNavigation,
} from 'src/app/navigation/types'
import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget'
import { useBiometricCheck } from 'src/features/biometrics/useBiometricCheck'
......@@ -75,6 +79,7 @@ import {
UnitagScreens,
UnitagStackParamList,
} from 'uniswap/src/types/screens/mobile'
import { isDevEnv } from 'utilities/src/environment/env'
import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors'
......@@ -332,6 +337,20 @@ export function UnitagStackNavigator(): JSX.Element {
export function AppStackNavigator(): JSX.Element {
const finishedOnboarding = useSelector(selectFinishedOnboarding)
useBiometricCheck()
const navigation = useAppStackNavigation()
useEffect(() => {
// Adds a menu item to navigate to Storybook in debug builds
if (__DEV__) {
DevSettings.addMenuItem('Toggle Storybook', () => {
if (navigationRef.getCurrentRoute()?.name === MobileScreens.Storybook) {
navigation.goBack()
} else {
navigation.navigate(MobileScreens.Storybook)
}
})
}
}, [navigation])
return (
<AppStack.Navigator
......@@ -362,6 +381,7 @@ export function AppStackNavigator(): JSX.Element {
<AppStack.Group screenOptions={navOptions.presentationModal}>
<AppStack.Screen component={EducationScreen} name={MobileScreens.Education} />
</AppStack.Group>
{isDevEnv() && <AppStack.Screen component={StorybookUIRoot} name={MobileScreens.Storybook} />}
</AppStack.Navigator>
)
}
......
......@@ -120,6 +120,7 @@ export type AppStackParamList = {
address: string
}
[MobileScreens.WebView]: { headerTitle: string; uriLink: string }
[MobileScreens.Storybook]: undefined
}
export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList>
......
......@@ -44,12 +44,15 @@ const dataDogReduxEnhancer = createDatadogReduxEnhancer({
},
})
const middlewares: Middleware[] = [getFiatOnRampAggregatorApi().middleware]
const enhancers = [dataDogReduxEnhancer]
if (isNonJestDev) {
const createDebugger = require('redux-flipper').default
middlewares.push(createDebugger())
const reactotron = require('src/../ReactotronConfig').default
enhancers.push(reactotron.createEnhancer())
}
const middlewares: Middleware[] = [getFiatOnRampAggregatorApi().middleware]
export const setupStore = (
preloadedState?: PreloadedState<MobileState>,
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
......@@ -59,7 +62,7 @@ export const setupStore = (
preloadedState,
additionalSagas: [rootMobileSaga],
middlewareAfter: [...middlewares],
enhancers: [dataDogReduxEnhancer],
enhancers,
})
}
export const store = setupStore()
......
......@@ -30,7 +30,9 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E
const isScanningQr = currentScreenState === ScannerModalState.ScanQr
const colors = useSporeColors(isScanningQr ? 'dark' : undefined)
const darkColors = useSporeColors('dark')
const themeColors = useSporeColors()
const colors = isScanningQr ? darkColors : themeColors
const onScanCode = async (uri: string): Promise<void> => {
if (shouldFreezeCamera) {
......
......@@ -64,7 +64,9 @@ export function WalletConnectModal({
const isScanningQr = currentScreenState === ScannerModalState.ScanQr
// We want to always show the QR Code Scanner in "dark mode"
const colors = useSporeColors(isScanningQr ? 'dark' : undefined)
const darkColors = useSporeColors('dark')
const themeColors = useSporeColors()
const colors = isScanningQr ? darkColors : themeColors
// Update QR scanner states when pending session error alert is shown from WCv2 saga event channel
useEffect(() => {
......
......@@ -2,9 +2,9 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { closeAllModals, closeModal } from 'src/features/modals/modalSlice'
import { closeAllModals } from 'src/features/modals/modalSlice'
import { Button, Flex, Text, useSporeColors } from 'ui/src'
import LockIcon from 'ui/src/assets/icons/lock.svg'
import { WalletFilled } from 'ui/src/components/icons'
import { iconSizes, opacify } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
......@@ -17,10 +17,6 @@ export function RestoreWalletModal(): JSX.Element | null {
const colors = useSporeColors()
const dispatch = useDispatch()
const onDismiss = (): void => {
dispatch(closeModal({ name: ModalName.RestoreWallet }))
}
const onRestore = (): void => {
dispatch(closeAllModals())
navigate(MobileScreens.OnboardingStack, {
......@@ -33,7 +29,7 @@ export function RestoreWalletModal(): JSX.Element | null {
}
return (
<Modal backgroundColor={colors.surface2.val} isDismissible={false} name={ModalName.RestoreWallet}>
<Modal hideHandlebar backgroundColor={colors.surface2.val} isDismissible={false} name={ModalName.RestoreWallet}>
<Flex centered gap="$spacing16" px="$spacing24" py="$spacing12">
<Flex
centered
......@@ -43,7 +39,7 @@ export function RestoreWalletModal(): JSX.Element | null {
backgroundColor: opacify(12, colors.neutral1.val),
}}
>
<LockIcon color={colors.neutral1.get()} height={iconSizes.icon24} width={iconSizes.icon24} />
<WalletFilled color="$neutral1" size={iconSizes.icon24} />
</Flex>
<Text textAlign="center" variant="body1">
{t('account.wallet.button.restore')}
......@@ -52,9 +48,6 @@ export function RestoreWalletModal(): JSX.Element | null {
{t('account.wallet.restore.description')}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing12">
<Button fill theme="tertiary" onPress={onDismiss}>
{t('common.button.dismiss')}
</Button>
<Button fill testID={TestID.RestoreWallet} theme="primary" onPress={onRestore}>
{t('common.button.restore')}
</Button>
......
import { ComponentMeta, ComponentStory } from '@storybook/react'
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import { CopyTextButton } from 'src/components/buttons/CopyTextButton'
import { StorybookTitles } from 'ui/src/storybook'
export default {
title: 'WIP/Button/Copy',
const meta = {
title: StorybookTitles.Atoms,
component: CopyTextButton,
} as ComponentMeta<typeof CopyTextButton>
} satisfies Meta<typeof CopyTextButton>
const Template: ComponentStory<typeof CopyTextButton> = (args) => <CopyTextButton {...args} />
type Story = StoryObj<typeof meta>
export const Primary = Template.bind({})
const CopyTextButtonStory: Story = {
storyName: 'CopyTextButton',
args: {
copyText: 'You copied me!',
},
}
export default meta
export { CopyTextButtonStory }
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'
import { Pill } from './Pill'
<Meta title="WIP/Pill/Primary" component={Pill} />
export const Template = (args) => <Pill {...args} onPress={() => void 0} />
# `Pill`
<Canvas>
<Story
name="default"
args={{
label: 'Your tokens',
borderColor: 'black',
}}
>
{Template.bind({})}
</Story>
</Canvas>
<ArgsTable story="default" />
// AppsFlyer automatically routes to the correct store based on the device
export const APP_STORE_LINK = 'https://uniswapwallet.onelink.me/8q3y/97upfib8'
export const APP_FEEDBACK_LINK =
'https://docs.google.com/forms/d/e/1FAIpQLSepzL5aMuSfRhSgw0zDw_gVmc2aeVevfrb1UbOwn6WGJ--46w/viewform'
import { Alert } from 'react-native'
import { MobileState } from 'src/app/mobileReducer'
import { APP_FEEDBACK_LINK } from 'src/constants/urls'
import { hasConsecutiveRecentSwapsSelector } from 'src/features/appRating/selectors'
import { Alert, Platform } from 'react-native'
import { call, delay, put, select, takeLatest } from 'typed-redux-saga'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { finalizeTransaction } from 'uniswap/src/features/transactions/slice'
......@@ -11,19 +9,21 @@ import i18n from 'uniswap/src/i18n/i18n'
import { openUri } from 'uniswap/src/utils/linking'
import { isJestRun } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger'
import { ONE_DAY_MS, ONE_SECOND_MS } from 'utilities/src/time/time'
import { isAndroid } from 'utilities/src/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { appRatingStateSelector } from 'wallet/src/features/appRating/selectors'
import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
import { setAppRating } from 'wallet/src/features/wallet/slice'
// at most once per reminder period (120 days)
const MIN_PROMPT_REMINDER_MS = 120 * ONE_DAY_MS
// remind after a longer delay when user filled the feedback form (180 days)
const MIN_FEEDBACK_REMINDER_MS = 180 * ONE_DAY_MS
function isAndroid14(): boolean {
return isAndroid && Platform.Version === 34
}
// small delay to help ux
const SWAP_FINALIZED_PROMPT_DELAY_MS = 3 * ONE_SECOND_MS
try {
if (!isJestRun) {
if (!isJestRun && !isAndroid14()) {
import('expo-store-review')
}
} catch (error) {
......@@ -72,24 +72,8 @@ function* maybeRequestAppRating() {
return
}
// Conditions
const appRatingProvidedMs = yield* select((state: MobileState) => state.wallet.appRatingProvidedMs)
if (appRatingProvidedMs) {
return
} // avoids prompting again
const appRatingPromptedMs = yield* select((state: MobileState) => state.wallet.appRatingPromptedMs)
const appRatingFeedbackProvidedMs = yield* select((state: MobileState) => state.wallet.appRatingFeedbackProvidedMs)
const consecutiveSwapsCondition = yield* select(hasConsecutiveRecentSwapsSelector)
// prompt if enough time has passed since last prompt or last feedback provided
const reminderCondition =
(appRatingPromptedMs !== undefined && Date.now() - appRatingPromptedMs > MIN_PROMPT_REMINDER_MS) ||
(appRatingFeedbackProvidedMs !== undefined && Date.now() - appRatingFeedbackProvidedMs > MIN_FEEDBACK_REMINDER_MS)
const hasNeverPrompted = appRatingPromptedMs === undefined
const shouldPrompt = consecutiveSwapsCondition && (hasNeverPrompted || reminderCondition)
const { shouldPrompt, appRatingProvidedMs, appRatingPromptedMs, consecutiveSwapsCondition } =
yield* select(appRatingStateSelector)
if (!shouldPrompt) {
logger.debug('appRating', 'maybeRequestAppRating', 'Skipping app rating', {
......@@ -152,9 +136,9 @@ function* maybeRequestAppRating() {
*/
async function openRatingOptionsAlert() {
return new Promise((resolve) => {
Alert.alert(i18n.t('mobile.appRating.title'), i18n.t('mobile.appRating.description'), [
Alert.alert(i18n.t('appRating.mobile.title'), i18n.t('appRating.description'), [
{
text: i18n.t('mobile.appRating.button.decline'),
text: i18n.t('appRating.button.notReally'),
onPress: () => resolve(false),
style: 'cancel',
},
......@@ -177,11 +161,11 @@ async function openRatingOptionsAlert() {
/** Opens feedback request modal which will redirect to our feedback form. */
async function openFeedbackRequestAlert() {
return new Promise((resolve) => {
Alert.alert(i18n.t('mobile.appRating.feedback.title'), i18n.t('mobile.appRating.feedback.description'), [
Alert.alert(i18n.t('appRating.feedback.title'), i18n.t('appRating.feedback.description'), [
{
text: i18n.t('mobile.appRating.feedback.button.send'),
text: i18n.t('appRating.feedback.button.send'),
onPress: () => {
openUri(APP_FEEDBACK_LINK).catch((e) =>
openUri(uniswapUrls.walletFeedbackForm).catch((e) =>
logger.error(e, { tags: { file: 'appRating/saga', function: 'openFeedbackAlert' } }),
)
resolve(true)
......@@ -189,7 +173,7 @@ async function openFeedbackRequestAlert() {
isPreferred: true,
},
{
text: i18n.t('mobile.appRating.feedback.button.cancel'),
text: i18n.t('common.button.later'),
onPress: () => resolve(false),
style: 'cancel',
},
......
......@@ -271,7 +271,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
))}
</Flex>
) : (
<TouchableArea onPress={onToggleIsTokenInputMode}>
<TouchableArea disabled={selectTokenLoading} onPress={onToggleIsTokenInputMode}>
<Flex
centered
row
......
......@@ -13,8 +13,6 @@ LogBox.ignoreLogs([
'logException:ApolloClient [Network Error]:',
// Ignore since it's difficult to filter out just these styles and they are often shared styles
'FlashList only supports padding related props and backgroundColor in contentContainerStyle.',
// This is enabled conditionally in bash profile only for dev mode
'The native module for Flipper seems unavailable.',
// https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#reduced-motion-setting-is-enabled-on-this-device
'[Reanimated] Reduced motion setting is enabled on this device.',
])
......@@ -103,3 +103,27 @@ require('@formatjs/intl-relativetimeformat/locale-data/tr').default
require('@formatjs/intl-relativetimeformat/locale-data/uk').default
require('@formatjs/intl-relativetimeformat/locale-data/ur').default
require('@formatjs/intl-relativetimeformat/locale-data/vi').default
// https://www.i18next.com/translation-function/formatting#list
require('@formatjs/intl-listformat/polyfill').default
// https://github.com/formatjs/formatjs/blob/main/packages/intl-listformat/supported-locales.generated.ts
require('@formatjs/intl-listformat/locale-data/zh-Hans').default
require('@formatjs/intl-listformat/locale-data/zh-Hant').default
require('@formatjs/intl-listformat/locale-data/nl').default
require('@formatjs/intl-listformat/locale-data/en').default
require('@formatjs/intl-listformat/locale-data/fr').default
require('@formatjs/intl-listformat/locale-data/hi').default
require('@formatjs/intl-listformat/locale-data/id').default
require('@formatjs/intl-listformat/locale-data/ja').default
require('@formatjs/intl-listformat/locale-data/ms').default
require('@formatjs/intl-listformat/locale-data/pt').default
require('@formatjs/intl-listformat/locale-data/ru').default
require('@formatjs/intl-listformat/locale-data/es').default
require('@formatjs/intl-listformat/locale-data/es-US').default
require('@formatjs/intl-listformat/locale-data/es-419').default
require('@formatjs/intl-listformat/locale-data/th').default
require('@formatjs/intl-listformat/locale-data/tr').default
require('@formatjs/intl-listformat/locale-data/uk').default
require('@formatjs/intl-listformat/locale-data/ur').default
require('@formatjs/intl-listformat/locale-data/vi').default
......@@ -16,7 +16,6 @@ import {
} from 'src/components/Settings/SettingsRow'
import { WalletSettings } from 'src/components/Settings/WalletSettings'
import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen'
import { APP_FEEDBACK_LINK } from 'src/constants/urls'
import { useBiometricContext } from 'src/features/biometrics/context'
import { useBiometricName, useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks'
import { useWalletRestore } from 'src/features/wallet/hooks'
......@@ -263,7 +262,7 @@ export function SettingsScreen(): JSX.Element {
{
screen: MobileScreens.WebView,
screenProps: {
uriLink: APP_FEEDBACK_LINK,
uriLink: uniswapUrls.walletFeedbackForm,
headerTitle: t('settings.action.feedback'),
},
text: t('settings.action.feedback'),
......
import { Meta } from '@storybook/addon-docs'
<Meta title="WIP/Getting Started" />
<style>{` `}</style>
# `@uniswap/mobile`
# 🎠
{/* TODO : Write about our design philosophy and how to use the design system. */}
## Philosophy
## Colors
## Layout
## Components
# Storybook
[Storybook](https://storybook.js.org/) helps build UI components in isolation. Testing and component documentation are built into the development workflow, leading to better tested and better documented component libraries.
## Useful resources
- [Official Getting Started](https://storybook.js.org/docs/react/get-started/introduction) by Storybook
- Our [Chromatic app](https://www.chromatic.com/builds?appId=61d89aa649fc7d003ae21c76)
- Our published [Storybook app](https://61d89aa649fc7d003ae21c76-gyrkwmtvsx.chromatic.com/)
## What components should have stories?
Literally any component can have a story, either for visual testing or documentation. I (judo) recommend we treat Storybook as a component library for now—that is, low-level presentational components that have high reusability and minimal dependencies (think spacing components, buttons, pills, etc.)
### Heuristic
If you expect your component to be imported by more than 3 other components, consider writing a story to document it (and get visual testing for free!)
### Recommendation
- Presentational components with short dependency list
- Components with hard to reach use cases (e.g. graph with mocked historical data)
- Design system philosophy (Storybook doesn't _require_ a component to be rendered, pages can also just be documentation)
- Design tokens
As our Storybook grows, this recommendation may extend to "compound" components, components that compose other components that have their own stories. The major benefit here is visual testing and documentation for new engineers.
## How to write stories?
Refer to [Storybook stories documentation](https://storybook.js.org/docs/react/writing-stories/introduction)
Storybook supports various `stories` formats.
- `.mdx`: Markdown with support for `jsx`. **No Typescript support**
- `.tsx`: Draw on canvas with Typescript
We're currently leaning on writing stories `.tsx` because of Typescript support.
### Run Storybook locally
To run Storybook locally and view stories:
```
yarn storybook
```
## Chromatic
Chromatic is a cloud service build for Storybook. It allows running visual tests with zero-config.
Chromatic is set up to build and publish our Storybook for each PR. ([example build](https://www.chromatic.com/build?appId=61d89aa649fc7d003ae21c76&number=25)).
In a PR, you should see 4 checks by Chromatic:
- chromatic-deployment workflow
- Storybook Publish: published Storybook to Chromatic
- [UI Review](https://www.chromatic.com/docs/review): invite reviewers to review the changeset introduced by your PR (code review, but for your UI)
- [UI Test](https://www.chromatic.com/docs/test): capture visual snapshot of every story and compares against baseline
## Going Forward
Storybook inside `@uniswap/mobile` is still experimental. Feel free to modify the config as you think would help!
### To research
- Accessability tests
- Interaction tests
### Addons
Storybook has a ton of [addons](https://storybook.js.org/addons/) that we could leverage / write our own.
- <https://storybook.js.org/addons/@storybook/addon-a11y>
### Open questions
- Figma integration? Can Figma re-use our low-level components and design tokens?
- Possibility to share Storybook with `react` (interface/widgets)
......@@ -2,6 +2,7 @@
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Mobile App",
"extends": "../../config/tsconfig/expo.json",
"exclude": [".storybook/storybook.requires.ts"],
"references": [
{
"path": "../../packages/ui"
......
......@@ -120,7 +120,7 @@
"eslint": "8.44.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-rulesdir": "0.2.2",
"hardhat": "2.14.0",
"hardhat": "2.22.16",
"husky": "8.0.3",
"jest": "29.7.0",
"jest-extended": "4.0.1",
......@@ -181,26 +181,26 @@
"@types/react-scroll-sync": "0.8.7",
"@types/react-window-infinite-loader": "1.0.6",
"@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.39.0",
"@uniswap/analytics-events": "2.40.0",
"@uniswap/client-explore": "0.0.12",
"@uniswap/client-pools": "0.0.9",
"@uniswap/liquidity-staker": "1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
"@uniswap/permit2-sdk": "1.3.0",
"@uniswap/redux-multicall": "1.1.8",
"@uniswap/router-sdk": "1.14.3",
"@uniswap/sdk-core": "5.9.0",
"@uniswap/router-sdk": "1.15.0",
"@uniswap/sdk-core": "6.0.0",
"@uniswap/smart-order-router": "3.17.3",
"@uniswap/token-lists": "1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18",
"@uniswap/universal-router-sdk": "4.5.2",
"@uniswap/universal-router-sdk": "4.7.0",
"@uniswap/v2-core": "1.0.1",
"@uniswap/v2-periphery": "1.1.0-beta.0",
"@uniswap/v2-sdk": "4.6.1",
"@uniswap/v2-sdk": "4.7.0",
"@uniswap/v3-core": "1.0.1",
"@uniswap/v3-periphery": "1.4.4",
"@uniswap/v3-sdk": "3.18.1",
"@uniswap/v4-sdk": "1.10.3",
"@uniswap/v3-sdk": "3.19.0",
"@uniswap/v4-sdk": "1.12.0",
"@vanilla-extract/css": "1.14.0",
"@vanilla-extract/dynamic": "2.1.0",
"@vanilla-extract/sprinkles": "1.6.1",
......@@ -256,7 +256,6 @@
"react-redux": "8.0.5",
"react-router-dom": "6.10.0",
"react-scroll-sync": "0.11.2",
"react-spring": "9.7.3",
"react-table": "7.8.0",
"react-use-gesture": "6.0.14",
"react-virtualized-auto-sizer": "1.0.20",
......
......@@ -126,16 +126,4 @@
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://app.uniswap.org/positions/create</loc>
<lastmod>2024-09-17T19:57:27.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://app.uniswap.org/positions</loc>
<lastmod>2024-09-17T19:57:27.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
</urlset>
......@@ -53,6 +53,7 @@
"https://cdn.center.app/",
"https://celo-org.github.io",
"https://cloudflare-eth.com",
"https://erasld2vrf.execute-api.us-east-2.amazonaws.com",
"https://ethereum-optimism.github.io/",
"https://forno.celo.org/",
"https://gateway.ipfs.io/",
......@@ -93,7 +94,9 @@
"https://vercel.live/",
"https://wallet.crypto.com",
"https://web3.1inch.io",
"https://x6ahx1oagk.execute-api.us-east-2.amazonaws.com",
"https://mainnet.era.zksync.io/",
"https://8mr3mthjba.execute-api.us-east-2.amazonaws.com",
"wss://*.uniswap.org",
"wss://relay.walletconnect.com",
"wss://relay.walletconnect.org",
......
......@@ -83,6 +83,17 @@
padding: 0;
}
/* Only apply overflow-x: hidden on desktop */
/* This is to prevent ugly horizontal scrollbar from appearing on desktop */
/* We need to set it on html element specifically because otherwise we break */
/* sticky positioning of some child elements. */
/* Applying this on mobile breaks tamagui/remove-scroll. */
@media (min-width: 768px) {
html {
overflow-x: hidden;
}
}
button {
user-select: none;
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -234,9 +234,9 @@ function CreateV3PoolSummary({ info: { quoteCurrencyId, baseCurrencyId } }: { in
)
}
function CollectFeesSummary({ info: { currencyId0, currencyId1 } }: { info: CollectFeesTransactionInfo }) {
const currency0 = useCurrency(currencyId0)
const currency1 = useCurrency(currencyId1)
function CollectFeesSummary({ info: { token0CurrencyId, token1CurrencyId } }: { info: CollectFeesTransactionInfo }) {
const currency0 = useCurrency(token0CurrencyId)
const currency1 = useCurrency(token1CurrencyId)
return (
<Trans
......
......@@ -3,27 +3,23 @@ import { ConfirmedIcon, LogoContainer, SubmittedIcon } from 'components/AccountD
import { useCancelOrdersGasEstimate } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks'
import { Container, Dialog, DialogButtonType, DialogProps } from 'components/Dialog/Dialog'
import { LoaderV3 } from 'components/Icons/LoadingSpinner'
import Modal from 'components/Modal'
import { GetHelpHeader } from 'components/Modal/GetHelpHeader'
import Column from 'components/deprecated/Column'
import Row from 'components/deprecated/Row'
import { DetailLineItem } from 'components/swap/DetailLineItem'
import styled, { useTheme } from 'lib/styled-components'
import { Slash } from 'react-feather'
import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types'
import { ExternalLink, ThemedText } from 'theme/components'
import { Flex } from 'ui/src'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { nativeOnChain } from 'uniswap/src/constants/tokens'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useUSDCValue } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice'
import { Plural, Trans, t } from 'uniswap/src/i18n'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { NumberType, useFormatter } from 'utils/formatNumbers'
const GasEstimateContainer = styled(Row)`
border-top: 1px solid ${({ theme }) => theme.surface3};
margin-top: 16px;
padding-top: 16px;
`
const ModalHeader = styled(GetHelpHeader)`
padding: 4px 0px;
`
......@@ -100,7 +96,7 @@ export function CancelOrdersDialog(
(cancelState === CancellationState.CANCELLED || cancelState === CancellationState.PENDING_CONFIRMATION) &&
cancelTxHash
return (
<Modal isOpen $scrollOverlay onDismiss={onCancel} maxHeight="90vh">
<Modal name={ModalName.CancelOrders} isModalOpen onClose={onCancel} maxHeight="90vh" padding={0}>
<Container gap="lg">
<ModalHeader closeModal={onCancel} />
<LogoContainer>{icon}</LogoContainer>
......@@ -131,14 +127,14 @@ export function CancelOrdersDialog(
icon={icon}
title={title}
description={
<Column>
<Flex width="100%">
<Plural
value={orders.length}
one={t('swap.cancel.cannotExecute')}
other={t('swap.cancel.cannotExecute.plural')}
/>
<GasEstimateDisplay chainId={orders[0].chainId} gasEstimateValue={gasEstimate?.value} />
</Column>
</Flex>
}
buttonsConfig={{
left: {
......@@ -170,14 +166,15 @@ function GasEstimateDisplay({ gasEstimateValue, chainId }: { gasEstimateValue?:
amount: gasFeeUSD,
type: NumberType.PortfolioBalance,
})
return (
<GasEstimateContainer>
<Flex row mt={16} pt={16} borderColor="$transparent" borderTopColor="$surface3" borderWidth={1} width="100%">
<DetailLineItem
LineItem={{
Label: () => <Trans i18nKey="common.networkCost" />,
Value: () => <span>{gasEstimateValue ? gasFeeFormatted : '-'}</span>,
}}
/>
</GasEstimateContainer>
</Flex>
)
}
......@@ -17,7 +17,6 @@ import { formatTimestamp } from 'components/AccountDrawer/MiniPortfolio/formatTi
import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button/buttons'
import { OpacityHoverState } from 'components/Common/styles'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import Modal from 'components/Modal'
import Column, { AutoColumn } from 'components/deprecated/Column'
import Row from 'components/deprecated/Row'
import { LimitDisclaimer } from 'components/swap/LimitDisclaimer'
......@@ -33,8 +32,9 @@ import { useOrder } from 'state/signatures/hooks'
import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types'
import { Divider, ThemedText } from 'theme/components'
import { UniswapXOrderStatus } from 'types/uniswapx'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { InterfaceEventNameLocal } from 'uniswap/src/features/telemetry/constants'
import { InterfaceEventNameLocal, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { Trans } from 'uniswap/src/i18n'
import { CurrencyField } from 'uniswap/src/types/currency'
......@@ -383,9 +383,11 @@ export function OffchainActivityModal() {
/>
)}
<Modal
name={ModalName.OffchainActivity}
maxWidth={375}
isOpen={!!selectedOrderAtomValue?.modalOpen && cancelState === CancellationState.NOT_STARTED}
onDismiss={reset}
isModalOpen={!!selectedOrderAtomValue?.modalOpen && cancelState === CancellationState.NOT_STARTED}
onClose={reset}
padding={0}
>
<Wrapper data-testid="offchain-activity-modal">
<Row justify="space-between">
......
......@@ -3,36 +3,6 @@
exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = `
<DocumentFragment>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c3 {
color: #222222;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
......@@ -40,17 +10,17 @@ exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = `
letter-spacing: -0.01em;
}
.c4 {
.c1 {
cursor: auto;
color: #7D7D7D;
}
.c5 {
.c2 {
text-align: right;
overflow-wrap: break-word;
}
.c6 {
.c3 {
background-color: transparent;
border: none;
cursor: pointer;
......@@ -93,23 +63,23 @@ exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = `
class=" t_light _dsp_contents is_Theme"
>
<div
class="c0 c1 c2"
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _alignItems-center _justifyContent-space-betwe3241 _width-10037"
>
<div
class="c3 c4 css-142zc9n"
class="c0 c1 css-142zc9n"
data-testid="swap-li-label"
>
Rate
</div>
<div
class="c3 c5 css-142zc9n"
class="c0 c2 css-142zc9n"
>
<button
class="c6"
class="c3"
title="1 USDC = <0.00001 DAI "
>
<div
class="c3 css-142zc9n"
class="c0 css-142zc9n"
>
1 USDC = &lt;0.00001 DAI
</div>
......@@ -126,36 +96,6 @@ exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = `
exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = `
<DocumentFragment>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c3 {
color: #222222;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
......@@ -163,12 +103,12 @@ exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = `
letter-spacing: -0.01em;
}
.c4 {
.c1 {
cursor: auto;
color: #7D7D7D;
}
.c5 {
.c2 {
text-align: right;
overflow-wrap: break-word;
}
......@@ -184,16 +124,16 @@ exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = `
class=" t_light _dsp_contents is_Theme"
>
<div
class="c0 c1 c2"
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _alignItems-center _justifyContent-space-betwe3241 _width-10037"
>
<div
class="c3 c4 css-142zc9n"
class="c0 c1 css-142zc9n"
data-testid="swap-li-label"
>
Network cost
</div>
<div
class="c3 c5 css-142zc9n"
class="c0 c2 css-142zc9n"
>
<span>
$0
......@@ -209,36 +149,6 @@ exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = `
exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = `
<DocumentFragment>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c3 {
color: #222222;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
......@@ -246,7 +156,7 @@ exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = `
letter-spacing: -0.01em;
}
.c6 {
.c3 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
......@@ -257,20 +167,20 @@ exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = `
font-weight: 500;
}
.c6:hover {
.c3:hover {
opacity: 0.6;
}
.c6:active {
.c3:active {
opacity: 0.4;
}
.c4 {
.c1 {
cursor: auto;
color: #7D7D7D;
}
.c5 {
.c2 {
text-align: right;
overflow-wrap: break-word;
}
......@@ -286,19 +196,19 @@ exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = `
class=" t_light _dsp_contents is_Theme"
>
<div
class="c0 c1 c2"
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _alignItems-center _justifyContent-space-betwe3241 _width-10037"
>
<div
class="c3 c4 css-142zc9n"
class="c0 c1 css-142zc9n"
data-testid="swap-li-label"
>
Transaction ID
</div>
<div
class="c3 c5 css-142zc9n"
class="c0 c2 css-142zc9n"
>
<a
class="c6"
class="c3"
href="https://etherscan.io/tx/0x123"
rel="noopener noreferrer"
target="_blank"
......
......@@ -211,10 +211,10 @@ jest.mock('../../../../state/transactions/hooks', () => {
...mockMultiStatus(
{
type: MockTxType.COLLECT_FEES,
currencyId0: MockUSDC_MAINNET.address,
currencyId1: MockDAI.address,
expectedCurrencyOwed0: mockCurrencyAmountRawUSDC,
expectedCurrencyOwed1: mockCurrencyAmountRaw,
token0CurrencyId: MockUSDC_MAINNET.address,
token1CurrencyId: MockDAI.address,
token0CurrencyAmountRaw: mockCurrencyAmountRawUSDC,
token1CurrencyAmountRaw: mockCurrencyAmountRaw,
},
'0xcollect_fees',
),
......
......@@ -217,10 +217,10 @@ async function parseCollectFees(
): Promise<Partial<Activity>> {
// Adapts CollectFeesTransactionInfo to generic LP type
const {
currencyId0: baseCurrencyId,
currencyId1: quoteCurrencyId,
expectedCurrencyOwed0: expectedAmountBaseRaw,
expectedCurrencyOwed1: expectedAmountQuoteRaw,
token0CurrencyId: baseCurrencyId,
token1CurrencyId: quoteCurrencyId,
token0CurrencyAmountRaw: expectedAmountBaseRaw,
token1CurrencyAmountRaw: expectedAmountQuoteRaw,
} = collect
return parseLegacyLP(
{ baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw },
......
import { InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
import MobileAppLogo from 'assets/svg/uniswap_app_logo.svg'
import Modal from 'components/Modal'
import { useConnect } from 'hooks/useConnect'
import { useCallback, useEffect, useState } from 'react'
import { CloseIcon } from 'theme/components'
import { Button, Flex, Image, QRCodeDisplay, Separator, Text, useSporeColors } from 'ui/src'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useTranslation } from 'uniswap/src/i18n'
import { isWebAndroid, isWebIOS } from 'utilities/src/platform'
......@@ -48,7 +49,7 @@ export default function UniwalletModal() {
const colors = useSporeColors()
return (
<Modal isOpen={open} onDismiss={close}>
<Modal name={ModalName.UniWalletConnect} isModalOpen={open} onClose={close} padding={0}>
<Flex shrink grow p="$spacing20">
<Flex row justifyContent="space-between">
<Text variant="subheading1">{t('account.drawer.modal.scan')}</Text>
......
......@@ -731,8 +731,18 @@ exports[`AccountDrawer tests AccountDrawer default styles 1`] = `
data-testid="wallet-modal"
>
<div
tabindex="0"
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column"
/>
<span
class="font_unset _display-inline _boxSizing-border-box _wordWrap-break-word _whiteSpace-pre-wrap _position-absolute _width-1px _height-1px _mt--1px _mr--1px _mb--1px _ml--1px _zIndex--10000 _overflowX-hidden _overflowY-hidden _opacity-1e-8 _pointerEvents-none"
>
<h2
class="is_DialogTitle font_heading _display-inline _boxSizing-border-box _wordWrap-break-word _fontFamily-f-family _color-color _fontWeight-f-weight-me3083741 _fontSize-f-size-medi3736 _lineHeight-f-lineHeigh507465454 _userSelect-auto _whiteSpace-normal _mt-0px _mr-0px _mb-0px _ml-0px"
data-disable-theme="true"
id="title-:r1:"
role="heading"
/>
</span>
<div
class="c7 c8 c9"
width="100%"
......
......@@ -7,11 +7,13 @@ import { useCallback } from 'react'
import { useModalIsOpen, useOpenModal, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { ThemedText } from 'theme/components'
import { AdaptiveWebModal, Flex, QRCodeDisplay, Text, useSporeColors } from 'ui/src'
import { Flex, QRCodeDisplay, Text, useSporeColors } from 'ui/src'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { NetworkLogos } from 'uniswap/src/components/network/NetworkLogos'
import { useAddressColorProps } from 'uniswap/src/features/address/color'
import { useOrderedChainIds } from 'uniswap/src/features/chains/hooks/useOrderedChainIds'
import { SUPPORTED_CHAIN_IDS } from 'uniswap/src/features/chains/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { Trans } from 'uniswap/src/i18n'
......@@ -35,7 +37,7 @@ export function AddressQRModal({ accountAddress }: { accountAddress: Address })
}, [toggleModal, openReceiveCryptoModal])
return (
<AdaptiveWebModal isOpen={isOpen} onClose={toggleModal} width={420}>
<Modal isModalOpen={isOpen} onClose={toggleModal} maxWidth={420} name={ModalName.AddressQR}>
<Flex pb="$spacing16" gap="$spacing24">
<GetHelpHeader goBack={goBack} closeModal={toggleModal} />
<Flex gap="$spacing12">
......@@ -76,6 +78,6 @@ export function AddressQRModal({ accountAddress }: { accountAddress: Address })
<NetworkLogos chains={orderedChainIds} />
</Flex>
</Flex>
</AdaptiveWebModal>
</Modal>
)
}
import AnimatedDropdown from 'components/AnimatedDropdown'
import { render, screen, waitFor } from 'test-utils/render'
describe('AnimatedDropdown', () => {
it('does not render children when closed', () => {
render(<AnimatedDropdown open={false}>Body</AnimatedDropdown>)
expect(screen.getByText('Body')).not.toBeVisible()
})
it('renders children when open', () => {
render(<AnimatedDropdown open={true}>Body</AnimatedDropdown>)
expect(screen.getByText('Body')).toBeVisible()
})
it('animates when open changes', async () => {
const { rerender } = render(<AnimatedDropdown open={false}>Body</AnimatedDropdown>)
const body = screen.getByText('Body')
expect(body).not.toBeVisible()
rerender(<AnimatedDropdown open={true}>Body</AnimatedDropdown>)
expect(body).not.toBeVisible()
// wait for React Spring animation to finish
await waitFor(() => {
expect(body).toBeVisible()
})
})
})
This diff is collapsed.
......@@ -18,6 +18,7 @@ export const HorizontalArea = ({
brushDomain,
selectedFill,
containerHeight,
containerWidth,
}: {
series: ChartEntry[]
xScale: ScaleLinear<number, number>
......@@ -26,6 +27,7 @@ export const HorizontalArea = ({
yValue: (d: ChartEntry) => number
brushDomain?: [number, number]
containerHeight: number
containerWidth: number
fill?: string
selectedFill?: string
}) => {
......@@ -42,9 +44,9 @@ export const HorizontalArea = ({
return (
<Bar
key={i}
x={xScale(0)}
x={xScale(xValue(d))}
y={yScale(price)}
width={xScale(xValue(d)) - xScale(0)}
width={xScale(containerWidth) - xScale(xValue(d))}
height={0.2}
fill={isInDomain ? selectedFill : fill}
rx={1}
......
......@@ -181,7 +181,7 @@ async function calculateActiveRangeTokensLocked(
}
/** Returns amounts of tokens locked in the given tick. Reference: https://docs.uniswap.org/sdk/v3/guides/advanced/active-liquidity */
async function calculateTokensLocked(
export async function calculateTokensLocked(
token0: Token,
token1: Token,
feeTier: FeeAmount,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment