ci(release): publish latest release

parent e140d212
...@@ -30,7 +30,6 @@ types ...@@ -30,7 +30,6 @@ types
apps/mobile/ios apps/mobile/ios
apps/mobile/android apps/mobile/android
apps/mobile/.storybook/storybook.requires.ts
# extension # extension
...@@ -43,7 +42,3 @@ packages/uniswap/codegen.ts ...@@ -43,7 +42,3 @@ packages/uniswap/codegen.ts
# eslint partials # eslint partials
packages/eslint-config/restrictedImports.js packages/eslint-config/restrictedImports.js
# generated
packages/uniswap/src/data/rest/conversionTracking/api
* @uniswap/web-admins
IPFS hash of the deployment: We are back with some new updates! Here’s the latest:
- CIDv0: `QmXuD12tmkqjiuUJcoGU2voXN2gYimddzrqQHjQrKaHKsT`
- CIDv1: `bafybeieocbd53xpdug6dkelfrtzn2r5vt4fn3j2vxnb73plzax7vwjsiqy`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). Token Warnings: See more information about the tokens you’re attempting to swap, enriched with data from Blockaid.
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://bafybeieocbd53xpdug6dkelfrtzn2r5vt4fn3j2vxnb73plzax7vwjsiqy.ipfs.dweb.link/
- https://bafybeieocbd53xpdug6dkelfrtzn2r5vt4fn3j2vxnb73plzax7vwjsiqy.ipfs.cf-ipfs.com/
- [ipfs://QmXuD12tmkqjiuUJcoGU2voXN2gYimddzrqQHjQrKaHKsT/](ipfs://QmXuD12tmkqjiuUJcoGU2voXN2gYimddzrqQHjQrKaHKsT/)
### 5.62.3 (2024-12-14)
### Bug Fixes
* **web:** modal height fix (#14539) 76ce85c
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
web/5.62.3 mobile/1.41
\ No newline at end of file \ No newline at end of file
...@@ -15,11 +15,11 @@ ...@@ -15,11 +15,11 @@
"@svgr/webpack": "8.0.1", "@svgr/webpack": "8.0.1",
"@tamagui/core": "1.114.4", "@tamagui/core": "1.114.4",
"@types/uuid": "9.0.1", "@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.40.0", "@uniswap/analytics-events": "2.39.0",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18", "@uniswap/uniswapx-sdk": "2.1.0-beta.18",
"@uniswap/universal-router-sdk": "4.7.0", "@uniswap/universal-router-sdk": "4.5.2",
"@uniswap/v3-sdk": "3.19.0", "@uniswap/v3-sdk": "3.18.1",
"@uniswap/v4-sdk": "1.12.0", "@uniswap/v4-sdk": "1.10.3",
"dotenv-webpack": "8.0.1", "dotenv-webpack": "8.0.1",
"ethers": "5.7.2", "ethers": "5.7.2",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
......
...@@ -6,7 +6,6 @@ import { ScreenHeader } from 'src/app/components/layout/ScreenHeader' ...@@ -6,7 +6,6 @@ import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { AccountItem } from 'src/app/features/accounts/AccountItem' import { AccountItem } from 'src/app/features/accounts/AccountItem'
import { CreateWalletModal } from 'src/app/features/accounts/CreateWalletModal' import { CreateWalletModal } from 'src/app/features/accounts/CreateWalletModal'
import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal' 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 { useDappContext } from 'src/app/features/dapp/DappContext'
import { updateDappConnectedAddressFromExtension } from 'src/app/features/dapp/actions' import { updateDappConnectedAddressFromExtension } from 'src/app/features/dapp/actions'
import { useDappConnectedAccounts } from 'src/app/features/dapp/hooks' import { useDappConnectedAccounts } from 'src/app/features/dapp/hooks'
...@@ -34,6 +33,7 @@ import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' ...@@ -34,6 +33,7 @@ import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PlusCircle } from 'wallet/src/components/icons/PlusCircle' import { PlusCircle } from 'wallet/src/components/icons/PlusCircle'
import { MenuContent } from 'wallet/src/components/menu/MenuContent' import { MenuContent } from 'wallet/src/components/menu/MenuContent'
import { MenuContentItem } from 'wallet/src/components/menu/types' 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 { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga' import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga'
...@@ -148,8 +148,17 @@ export function AccountSwitcherScreen(): JSX.Element { ...@@ -148,8 +148,17 @@ export function AccountSwitcherScreen(): JSX.Element {
onPress: (): void => setShowRemoveWalletModal(true), onPress: (): void => setShowRemoveWalletModal(true),
}, },
] ]
const { data: accountBalanceData } = useAccountList({
addresses: accountAddresses,
notifyOnNetworkStatusChange: true,
})
const sortedAddressesByBalance = useSortedAccountList(accountAddresses) 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 contentShadowProps = { const contentShadowProps = {
shadowColor: colors.shadowColor.val, 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,7 +14,9 @@ import { SignTypedDataRequestContent } from 'src/app/features/dappRequests/reque ...@@ -14,7 +14,9 @@ import { SignTypedDataRequestContent } from 'src/app/features/dappRequests/reque
import { rejectAllRequests } from 'src/app/features/dappRequests/saga' import { rejectAllRequests } from 'src/app/features/dappRequests/saga'
import { isDappRequestStoreItemForEthSendTxn } from 'src/app/features/dappRequests/slice' import { isDappRequestStoreItemForEthSendTxn } from 'src/app/features/dappRequests/slice'
import { import {
isConnectionRequest, isGetAccountRequest,
isRequestAccountRequest,
isRequestPermissionsRequest,
isSignMessageRequest, isSignMessageRequest,
isSignTypedDataRequest, isSignTypedDataRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
...@@ -199,7 +201,11 @@ const DappRequest = memo(function _DappRequest(): JSX.Element | null { ...@@ -199,7 +201,11 @@ const DappRequest = memo(function _DappRequest(): JSX.Element | null {
if (isDappRequestStoreItemForEthSendTxn(request)) { if (isDappRequestStoreItemForEthSendTxn(request)) {
return <EthSendRequestContent request={request} /> return <EthSendRequestContent request={request} />
} }
if (isConnectionRequest(request.dappRequest)) { if (
isGetAccountRequest(request.dappRequest) ||
isRequestAccountRequest(request.dappRequest) ||
isRequestPermissionsRequest(request.dappRequest)
) {
return <ConnectionRequestContent /> return <ConnectionRequestContent />
} }
......
...@@ -2,7 +2,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' ...@@ -2,7 +2,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { DappInfo } from 'src/app/features/dapp/store' import { DappInfo } from 'src/app/features/dapp/store'
import { import {
DappRequest, DappRequest,
isConnectionRequest,
isSendTransactionRequest, isSendTransactionRequest,
SendTransactionRequest, SendTransactionRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
...@@ -43,14 +42,6 @@ const slice = createSlice({ ...@@ -43,14 +42,6 @@ const slice = createSlice({
initialState: initialDappRequestState, initialState: initialDappRequestState,
reducers: { reducers: {
add: (state, action: PayloadAction<DappRequestStoreItem>) => { 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) state.pending.push(action.payload)
}, },
remove: (state, action: PayloadAction<string>) => { remove: (state, action: PayloadAction<string>) => {
......
...@@ -384,12 +384,3 @@ export function isRequestAccountRequest(request: DappRequest): request is Reques ...@@ -384,12 +384,3 @@ export function isRequestAccountRequest(request: DappRequest): request is Reques
export function isRequestPermissionsRequest(request: DappRequest): request is RequestPermissionsRequest { export function isRequestPermissionsRequest(request: DappRequest): request is RequestPermissionsRequest {
return RequestPermissionsRequestSchema.safeParse(request).success return RequestPermissionsRequestSchema.safeParse(request).success
} }
export function isConnectionRequest(request: DappRequest): boolean {
return (
isGetAccountRequest(request) ||
isRequestAccountRequest(request) ||
isRequestPermissionsRequest(request)
)
}
...@@ -5,8 +5,6 @@ import { useTranslation } from 'react-i18next' ...@@ -5,8 +5,6 @@ import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { ActivityTab } from 'src/app/components/tabs/ActivityTab' import { ActivityTab } from 'src/app/components/tabs/ActivityTab'
import { NftsTab } from 'src/app/components/tabs/NftsTab' 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 { PortfolioActionButtons } from 'src/app/features/home/PortfolioActionButtons'
import { PortfolioHeader } from 'src/app/features/home/PortfolioHeader' import { PortfolioHeader } from 'src/app/features/home/PortfolioHeader'
import { TokenBalanceList } from 'src/app/features/home/TokenBalanceList' import { TokenBalanceList } from 'src/app/features/home/TokenBalanceList'
...@@ -113,8 +111,6 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element { ...@@ -113,8 +111,6 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element {
} }
}, [apolloClient, shouldRefetchNfts]) }, [apolloClient, shouldRefetchNfts])
const { appRatingModalVisible, onAppRatingModalClose } = useAppRating()
return ( return (
<Flex fill alignItems="center" backgroundColor="$surface1" p="$spacing12"> <Flex fill alignItems="center" backgroundColor="$surface1" p="$spacing12">
{address ? ( {address ? (
...@@ -197,7 +193,6 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element { ...@@ -197,7 +193,6 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element {
{t('home.extension.error')} {t('home.extension.error')}
</Text> </Text>
)} )}
{appRatingModalVisible && <AppRatingModal onClose={onAppRatingModalClose} />}
</Flex> </Flex>
) )
}) })
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Uniswap Extension", "name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.12.0", "version": "1.11.0",
"minimum_chrome_version": "116", "minimum_chrome_version": "116",
"icons": { "icons": {
"16": "assets/icon16.png", "16": "assets/icon16.png",
......
module.exports = { module.exports = {
root: true, root: true,
extends: ['@uniswap/eslint-config/native'], extends: ['@uniswap/eslint-config/native'],
ignorePatterns: ['.storybook/storybook.requires.ts'],
parserOptions: { parserOptions: {
project: 'tsconfig.eslint.json', project: 'tsconfig.eslint.json',
tsconfigRootDir: __dirname, 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 ...@@ -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. 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). See `getFingerprintForRadonIDE.js` for more details. 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). There are more complex implementations of this, but this is a simple first step.
#### Running on a Physical iOS Device #### Running on a Physical iOS Device
...@@ -240,6 +240,17 @@ In `.vscode/launch.json`, you will see configurations for each platform. This is ...@@ -240,6 +240,17 @@ 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 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 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 ## 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. These are some tools you might want to familiarize yourself with to understand the codebase better and how different aspects of it work.
...@@ -313,3 +324,8 @@ eval "$(/opt/homebrew/bin/brew shellenv)" ...@@ -313,3 +324,8 @@ eval "$(/opt/homebrew/bin/brew shellenv)"
export NVM_DIR="$HOME/.nvm" 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/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 [ -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) { ...@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
} }
def devVersionName = "1.42" def devVersionName = "1.41"
def betaVersionName = "1.42" def betaVersionName = "1.41"
def prodVersionName = "1.42" def prodVersionName = "1.41"
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
...@@ -104,6 +104,7 @@ android { ...@@ -104,6 +104,7 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
...@@ -208,6 +209,9 @@ dependencies { ...@@ -208,6 +209,9 @@ dependencies {
implementation "com.facebook.react:react-android" implementation "com.facebook.react:react-android"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" 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.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerialization" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerialization"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle"
...@@ -251,6 +255,8 @@ dependencies { ...@@ -251,6 +255,8 @@ dependencies {
implementation("androidx.core:core-performance:$corePerf") implementation("androidx.core:core-performance:$corePerf")
implementation("androidx.core:core-performance-play-services:$corePerf") implementation("androidx.core:core-performance-play-services:$corePerf")
implementation("com.facebook.react:flipper-integration")
if (hermesEnabled.toBoolean()) { if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android") implementation("com.facebook.react:hermes-android")
} else { } else {
......
package com.uniswap package com.uniswap
import android.app.Application
import android.content.res.Configuration import android.content.res.Configuration
import androidx.multidex.MultiDexApplication
import com.facebook.react.PackageList import com.facebook.react.PackageList
import com.facebook.react.ReactApplication import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader import com.facebook.soloader.SoLoader
import com.shopify.reactnativeperformance.ReactNativePerformance import com.shopify.reactnativeperformance.ReactNativePerformance
import com.uniswap.onboarding.scantastic.ScantasticEncryptionModule import com.uniswap.onboarding.scantastic.ScantasticEncryptionModule
import expo.modules.ApplicationLifecycleDispatcher import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper import expo.modules.ReactNativeHostWrapper
import com.uniswap.RedirectToSourceAppPackage
class MainApplication : Application(), ReactApplication { class MainApplication : MultiDexApplication(), ReactApplication {
override val reactNativeHost: ReactNativeHost = override val reactNativeHost: ReactNativeHost =
ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) { ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> = override fun getPackages(): List<ReactPackage> =
...@@ -54,6 +57,7 @@ class MainApplication : Application(), ReactApplication { ...@@ -54,6 +57,7 @@ class MainApplication : Application(), ReactApplication {
// If you opted-in for the New Architecture, we load the native entry point for this app. // If you opted-in for the New Architecture, we load the native entry point for this app.
load() load()
} }
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager);
ApplicationLifecycleDispatcher.onApplicationCreate(this) ApplicationLifecycleDispatcher.onApplicationCreate(this)
} }
......
...@@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalDensity ...@@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.uniswap.onboarding.shared.CopyButton import com.uniswap.onboarding.shared.CopyButton
import com.uniswap.theme.UniswapTheme
import com.uniswap.theme.relativeOffset import com.uniswap.theme.relativeOffset
import kotlin.math.abs import kotlin.math.abs
...@@ -72,8 +73,7 @@ fun MnemonicDisplay( ...@@ -72,8 +73,7 @@ fun MnemonicDisplay(
CopyButton( CopyButton(
copyButtonText = copyText, copyButtonText = copyText,
copiedButtonText = copiedText, copiedButtonText = copiedText,
textToCopy = textToCopy, textToCopy = textToCopy
isSensitive = true
) )
} }
} }
......
package com.uniswap.onboarding.shared 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.annotation.DrawableRes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
...@@ -18,7 +12,6 @@ import androidx.compose.foundation.layout.size ...@@ -18,7 +12,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
...@@ -29,15 +22,12 @@ import androidx.compose.ui.Modifier ...@@ -29,15 +22,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.uniswap.R import com.uniswap.R
import com.uniswap.theme.UniswapTheme import com.uniswap.theme.UniswapTheme
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
enum class ActionButtonStatus { enum class ActionButtonStatus {
SUCCESS, NEUTRAL SUCCESS, NEUTRAL
...@@ -98,37 +88,14 @@ fun ActionButton( ...@@ -98,37 +88,14 @@ 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 @Composable
fun CopyButton( fun CopyButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
copyButtonText: String, copyButtonText: String,
copiedButtonText: String, copiedButtonText: String,
textToCopy: AnnotatedString, textToCopy: AnnotatedString
isSensitive: Boolean = false,
) { ) {
val context = LocalContext.current val clipboardManager = LocalClipboardManager.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 // Time after which the button reverts to the copy state
val copyTimeout: Long = 2000 val copyTimeout: Long = 2000
...@@ -136,28 +103,11 @@ fun CopyButton( ...@@ -136,28 +103,11 @@ fun CopyButton(
var copyTimeoutId by remember { mutableStateOf(0) } var copyTimeoutId by remember { mutableStateOf(0) }
fun onClick() { fun onClick() {
val label = if (isSensitive) "sensitive data" else "copied text" clipboardManager.setText(textToCopy)
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++ copyTimeoutId++
isCopied = true isCopied = true
} }
LaunchedEffect(copyTimeoutId) { LaunchedEffect(copyTimeoutId) {
delay(copyTimeout) delay(copyTimeout)
isCopied = false isCopied = false
......
...@@ -15,6 +15,7 @@ buildscript { ...@@ -15,6 +15,7 @@ buildscript {
flowlayout = "0.23.1" flowlayout = "0.23.1"
kotlinSerialization = "1.5.1" kotlinSerialization = "1.5.1"
lifecycle = "2.5.1" lifecycle = "2.5.1"
multidexVersion = "2.0.1"
} }
repositories { repositories {
google() google()
......
...@@ -26,6 +26,9 @@ android.useAndroidX=true ...@@ -26,6 +26,9 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true 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. # Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead. # If set to false, you will be using JSC instead.
hermesEnabled=true hermesEnabled=true
// Disable sorting imports with Prettier for this file so that it doesn't change the order // Disable sorting imports with Prettier for this file so that it doesn't change the order
// organize-imports-ignore // organize-imports-ignore
import './wdyr' import './wdyr'
import { isNonJestDev } from 'utilities/src/environment/constants'
if (isNonJestDev) {
require('./ReactotronConfig')
}
import { AppRegistry } from 'react-native' import { AppRegistry } from 'react-native'
import 'react-native-gesture-handler' import 'react-native-gesture-handler'
......
...@@ -28,10 +28,13 @@ target 'Uniswap' do ...@@ -28,10 +28,13 @@ target 'Uniswap' do
use_expo_modules!(exclude: ['expo-constants','expo-file-system', 'expo-font', 'expo-keep-awake', 'expo-error-recovery']) use_expo_modules!(exclude: ['expo-constants','expo-file-system', 'expo-font', 'expo-keep-awake', 'expo-error-recovery'])
config = use_native_modules! config = use_native_modules!
flipper_config = ENV['USE_FLIPPER'] ? FlipperConfiguration.enabled : FlipperConfiguration.disabled
use_react_native!( use_react_native!(
:path => config[:reactNativePath], :path => config[:reactNativePath],
# to enable hermes on iOS, change `false` to `true` and then install pods # to enable hermes on iOS, change `false` to `true` and then install pods
:hermes_enabled => true :hermes_enabled => true,
:flipper_configuration => flipper_config
) )
target 'UniswapTests' do target 'UniswapTests' do
......
...@@ -2364,10 +2364,6 @@ PODS: ...@@ -2364,10 +2364,6 @@ PODS:
- React - React
- React-callinvoker - React-callinvoker
- React-Core - React-Core
- react-native-slider (4.5.5):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- react-native-webview (11.23.1): - react-native-webview (11.23.1):
- React-Core - React-Core
- react-native-widgetkit (1.0.9): - react-native-widgetkit (1.0.9):
...@@ -2546,8 +2542,6 @@ PODS: ...@@ -2546,8 +2542,6 @@ PODS:
- React-Core - React-Core
- RNCMaskedView (0.2.9): - RNCMaskedView (0.2.9):
- React-Core - React-Core
- RNDateTimePicker (8.2.0):
- React-Core
- RNDeviceInfo (10.0.2): - RNDeviceInfo (10.0.2):
- React-Core - React-Core
- RNFastImage (8.6.3): - RNFastImage (8.6.3):
...@@ -2688,7 +2682,6 @@ DEPENDENCIES: ...@@ -2688,7 +2682,6 @@ DEPENDENCIES:
- react-native-restart (from `../../../node_modules/react-native-restart`) - react-native-restart (from `../../../node_modules/react-native-restart`)
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) - 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-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-webview (from `../../../node_modules/react-native-webview`)
- react-native-widgetkit (from `../../../node_modules/react-native-widgetkit`) - react-native-widgetkit (from `../../../node_modules/react-native-widgetkit`)
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`) - React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
...@@ -2714,7 +2707,6 @@ DEPENDENCIES: ...@@ -2714,7 +2707,6 @@ DEPENDENCIES:
- "ReactNativePerformance (from `../../../node_modules/@shopify/react-native-performance`)" - "ReactNativePerformance (from `../../../node_modules/@shopify/react-native-performance`)"
- "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)" - "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
- "RNCMaskedView (from `../../../node_modules/@react-native-masked-view/masked-view`)" - "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`) - RNDeviceInfo (from `../../../node_modules/react-native-device-info`)
- RNFastImage (from `../../../node_modules/react-native-fast-image`) - RNFastImage (from `../../../node_modules/react-native-fast-image`)
- "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)" - "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)"
...@@ -2903,8 +2895,6 @@ EXTERNAL SOURCES: ...@@ -2903,8 +2895,6 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native-safe-area-context" :path: "../../../node_modules/react-native-safe-area-context"
react-native-skia: react-native-skia:
:path: "../../../node_modules/@shopify/react-native-skia" :path: "../../../node_modules/@shopify/react-native-skia"
react-native-slider:
:path: "../../../node_modules/@react-native-community/slider"
react-native-webview: react-native-webview:
:path: "../../../node_modules/react-native-webview" :path: "../../../node_modules/react-native-webview"
react-native-widgetkit: react-native-widgetkit:
...@@ -2955,8 +2945,6 @@ EXTERNAL SOURCES: ...@@ -2955,8 +2945,6 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/@react-native-async-storage/async-storage" :path: "../../../node_modules/@react-native-async-storage/async-storage"
RNCMaskedView: RNCMaskedView:
:path: "../../../node_modules/@react-native-masked-view/masked-view" :path: "../../../node_modules/@react-native-masked-view/masked-view"
RNDateTimePicker:
:path: "../../../node_modules/@react-native-community/datetimepicker"
RNDeviceInfo: RNDeviceInfo:
:path: "../../../node_modules/react-native-device-info" :path: "../../../node_modules/react-native-device-info"
RNFastImage: RNFastImage:
...@@ -3090,7 +3078,6 @@ SPEC CHECKSUMS: ...@@ -3090,7 +3078,6 @@ SPEC CHECKSUMS:
react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162
react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b
react-native-skia: c7d8d8a380d4dbf8463343e2738bceaf49a9c084 react-native-skia: c7d8d8a380d4dbf8463343e2738bceaf49a9c084
react-native-slider: 708dea8f20c91a325d622fe8e11d3b9dfff158a3
react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581 react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581
react-native-widgetkit: efb6680df237463bbe1be3a4d1a1578a1b0bb08f react-native-widgetkit: efb6680df237463bbe1be3a4d1a1578a1b0bb08f
React-nativeconfig: e700ac3ec3d66329076bd2c2787a204411815d43 React-nativeconfig: e700ac3ec3d66329076bd2c2787a204411815d43
...@@ -3117,7 +3104,6 @@ SPEC CHECKSUMS: ...@@ -3117,7 +3104,6 @@ SPEC CHECKSUMS:
RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21
RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca
RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6 RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6
RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14
RNDeviceInfo: 0a7c1d2532aa7691f9b9925a27e43af006db4dae RNDeviceInfo: 0a7c1d2532aa7691f9b9925a27e43af006db4dae
RNFastImage: 246de6b52d7642992cfd01e2005dda36d00a6660 RNFastImage: 246de6b52d7642992cfd01e2005dda36d00a6660
RNFBApp: 4122dd41d8d7ff017b6ecf777a6224f5b349ca04 RNFBApp: 4122dd41d8d7ff017b6ecf777a6224f5b349ca04
...@@ -3140,6 +3126,6 @@ SPEC CHECKSUMS: ...@@ -3140,6 +3126,6 @@ SPEC CHECKSUMS:
Yoga: 805bf71192903b20fc14babe48080582fee65a80 Yoga: 805bf71192903b20fc14babe48080582fee65a80
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 525fd4a1c78879023ae05970b18e66b654c4c07a PODFILE CHECKSUM: e4219214e71cc0c904d669166daa324a8e54be8f
COCOAPODS: 1.14.3 COCOAPODS: 1.14.3
...@@ -164,6 +164,7 @@ ...@@ -164,6 +164,7 @@
8EE7C0582AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE7C0572AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift */; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; 91D501722CDBEAE700B09B7F /* TokenProjectMarketsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D501672CDBEAE700B09B7F /* TokenProjectMarketsParts.graphql.swift */; };
...@@ -2204,7 +2205,7 @@ ...@@ -2204,7 +2205,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2257,7 +2258,7 @@ ...@@ -2257,7 +2258,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2310,7 +2311,7 @@ ...@@ -2310,7 +2311,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2363,7 +2364,7 @@ ...@@ -2363,7 +2364,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2401,7 +2402,7 @@ ...@@ -2401,7 +2402,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2437,7 +2438,7 @@ ...@@ -2437,7 +2438,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2472,7 +2473,7 @@ ...@@ -2472,7 +2473,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2507,7 +2508,7 @@ ...@@ -2507,7 +2508,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2554,7 +2555,7 @@ ...@@ -2554,7 +2555,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2600,7 +2601,7 @@ ...@@ -2600,7 +2601,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
...@@ -2646,7 +2647,7 @@ ...@@ -2646,7 +2647,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
...@@ -2692,7 +2693,7 @@ ...@@ -2692,7 +2693,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
...@@ -2734,7 +2735,7 @@ ...@@ -2734,7 +2735,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2777,7 +2778,7 @@ ...@@ -2777,7 +2778,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
...@@ -2820,7 +2821,7 @@ ...@@ -2820,7 +2821,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
...@@ -2863,7 +2864,7 @@ ...@@ -2863,7 +2864,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
...@@ -2899,7 +2900,7 @@ ...@@ -2899,7 +2900,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -2937,7 +2938,7 @@ ...@@ -2937,7 +2938,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3137,7 +3138,7 @@ ...@@ -3137,7 +3138,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -3181,7 +3182,7 @@ ...@@ -3181,7 +3182,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
...@@ -3292,7 +3293,7 @@ ...@@ -3292,7 +3293,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3363,7 +3364,7 @@ ...@@ -3363,7 +3364,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
...@@ -3474,7 +3475,7 @@ ...@@ -3474,7 +3475,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3545,7 +3546,7 @@ ...@@ -3545,7 +3546,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.41;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
...@@ -7,10 +7,8 @@ ...@@ -7,10 +7,8 @@
const { getMetroAndroidAssetsResolutionFix } = require('react-native-monorepo-tools') const { getMetroAndroidAssetsResolutionFix } = require('react-native-monorepo-tools')
const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix() const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix()
const withStorybook = require('@storybook/react-native/metro/withStorybook')
const path = require('path') 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 mobileRoot = path.resolve(__dirname)
const workspaceRoot = path.resolve(mobileRoot, '../..') const workspaceRoot = path.resolve(mobileRoot, '../..')
...@@ -19,18 +17,18 @@ const watchFolders = [mobileRoot, `${workspaceRoot}/node_modules`, `${workspaceR ...@@ -19,18 +17,18 @@ const watchFolders = [mobileRoot, `${workspaceRoot}/node_modules`, `${workspaceR
const detoxExtensions = process.env.DETOX_MODE === 'mocked' ? ['mock.tsx', 'mock.ts'] : [] const detoxExtensions = process.env.DETOX_MODE === 'mocked' ? ['mock.tsx', 'mock.ts'] : []
const defaultConfig = getDefaultConfig(__dirname) const defaultConfig = getDefaultConfig(__dirname);
const { const {
resolver: { sourceExts, assetExts }, resolver: { sourceExts, assetExts },
} = defaultConfig } = defaultConfig;
const config = { const config = {
resolver: { resolver: {
nodeModulesPaths: [`${workspaceRoot}/node_modules`], nodeModulesPaths: [`${workspaceRoot}/node_modules`],
assetExts: assetExts.filter((ext) => ext !== 'svg'), assetExts: assetExts.filter((ext) => ext !== 'svg'),
// detox mocking works properly only being spreaded at the beginning of sourceExts array // detox mocking works properly only being spreaded at the beginning of sourceExts array
sourceExts: [...detoxExtensions, ...sourceExts, 'svg', 'cjs'], sourceExts: [...detoxExtensions, ...sourceExts, 'svg', 'cjs']
}, },
transformer: { transformer: {
getTransformOptions: async () => ({ getTransformOptions: async () => ({
...@@ -50,11 +48,4 @@ const config = { ...@@ -50,11 +48,4 @@ const config = {
watchFolders, watchFolders,
} }
// Checkout more useful options in the docs: https://github.com/storybookjs/react-native?tab=readme-ov-file#options module.exports = mergeConfig(defaultConfig, config);
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,7 +13,6 @@ ...@@ -13,7 +13,6 @@
"check:deps:usage": "./scripts/checkDepsUsage.sh", "check:deps:usage": "./scripts/checkDepsUsage.sh",
"clean": "react-native-clean-project", "clean": "react-native-clean-project",
"debug": "react-devtools", "debug": "react-devtools",
"debug:reactotron:install": "./scripts/installDebugger.sh",
"deduplicate": "yarn-deduplicate --strategy=fewer", "deduplicate": "yarn-deduplicate --strategy=fewer",
"depcheck": "depcheck", "depcheck": "depcheck",
"env:android:keystore:download": "bash ./scripts/downloadAndroidKeystore.sh", "env:android:keystore:download": "bash ./scripts/downloadAndroidKeystore.sh",
...@@ -33,7 +32,8 @@ ...@@ -33,7 +32,8 @@
"firestore:deploy:rules": "firebase deploy --only firestore:rules", "firestore:deploy:rules": "firebase deploy --only firestore:rules",
"link:assets": "react-native-asset", "link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate", "graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 8", "hardhat": "hardhat node",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 10",
"ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios", "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: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)\"", "ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"",
...@@ -50,9 +50,7 @@ ...@@ -50,9 +50,7 @@
"snapshots": "jest -u", "snapshots": "jest -u",
"typecheck": "tsc -b", "typecheck": "tsc -b",
"unicons": "cd scripts && python3 populate_svgs.py && cd .. && yarn lint --fix", "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": { "dependencies": {
"@amplitude/analytics-react-native": "1.4.0", "@amplitude/analytics-react-native": "1.4.0",
...@@ -62,7 +60,6 @@ ...@@ -62,7 +60,6 @@
"@ethersproject/shims": "5.6.0", "@ethersproject/shims": "5.6.0",
"@formatjs/intl-datetimeformat": "4.5.1", "@formatjs/intl-datetimeformat": "4.5.1",
"@formatjs/intl-getcanonicallocales": "1.9.0", "@formatjs/intl-getcanonicallocales": "1.9.0",
"@formatjs/intl-listformat": "7.7.5",
"@formatjs/intl-locale": "2.4.44", "@formatjs/intl-locale": "2.4.44",
"@formatjs/intl-numberformat": "7.4.1", "@formatjs/intl-numberformat": "7.4.1",
"@formatjs/intl-pluralrules": "4.3.1", "@formatjs/intl-pluralrules": "4.3.1",
...@@ -88,10 +85,10 @@ ...@@ -88,10 +85,10 @@
"@shopify/react-native-skia": "1.4.2", "@shopify/react-native-skia": "1.4.2",
"@sparkfabrik/react-native-idfa-aaid": "1.2.0", "@sparkfabrik/react-native-idfa-aaid": "1.2.0",
"@uniswap/analytics": "1.7.0", "@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.40.0", "@uniswap/analytics-events": "2.39.0",
"@uniswap/client-explore": "0.0.12", "@uniswap/client-explore": "0.0.12",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "6.0.0", "@uniswap/sdk-core": "5.9.0",
"@walletconnect/core": "2.17.1", "@walletconnect/core": "2.17.1",
"@walletconnect/react-native-compat": "2.17.1", "@walletconnect/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1", "@walletconnect/utils": "2.17.1",
...@@ -165,11 +162,7 @@ ...@@ -165,11 +162,7 @@
"@babel/runtime": "7.18.9", "@babel/runtime": "7.18.9",
"@datadog/datadog-ci": "2.39.0", "@datadog/datadog-ci": "2.39.0",
"@faker-js/faker": "7.6.0", "@faker-js/faker": "7.6.0",
"@react-native-community/datetimepicker": "8.2.0", "@storybook/react": "7.0.2",
"@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", "@tamagui/babel-plugin": "1.114.4",
"@testing-library/react-native": "11.5.0", "@testing-library/react-native": "11.5.0",
"@types/redux-mock-store": "1.0.6", "@types/redux-mock-store": "1.0.6",
...@@ -183,6 +176,7 @@ ...@@ -183,6 +176,7 @@
"detox": "20.23.0", "detox": "20.23.0",
"eslint": "8.44.0", "eslint": "8.44.0",
"expo-modules-core": "1.11.13", "expo-modules-core": "1.11.13",
"hardhat": "2.14.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-expo": "50.0.4", "jest-expo": "50.0.4",
"jest-extended": "4.0.1", "jest-extended": "4.0.1",
...@@ -195,12 +189,11 @@ ...@@ -195,12 +189,11 @@
"react-native-asset": "2.1.1", "react-native-asset": "2.1.1",
"react-native-clean-project": "4.0.1", "react-native-clean-project": "4.0.1",
"react-native-dotenv": "3.2.0", "react-native-dotenv": "3.2.0",
"react-native-flipper": "0.212.0",
"react-native-monorepo-tools": "1.2.1", "react-native-monorepo-tools": "1.2.1",
"react-native-svg-transformer": "1.3.0", "react-native-svg-transformer": "1.3.0",
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"reactotron-react-native": "5.1.10", "redux-flipper": "2.0.2",
"reactotron-react-native-mmkv": "0.2.7",
"reactotron-redux": "3.1.10",
"redux-saga-test-plan": "4.0.4", "redux-saga-test-plan": "4.0.4",
"typescript": "5.3.3", "typescript": "5.3.3",
"yarn-deduplicate": "6.0.0" "yarn-deduplicate": "6.0.0"
......
module.exports = { module.exports = {
assets: ['./src/assets/fonts'], 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 // 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 // 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 // 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') 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 { ApolloProvider } from '@apollo/client'
import { DdRum, DdSdkReactNative } from '@datadog/mobile-react-native' import {
DatadogProvider,
DatadogProviderConfiguration,
DdRum,
DdSdkReactNative,
SdkVerbosity,
} from '@datadog/mobile-react-native'
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance' import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance'
import { MMKVWrapper } from 'apollo3-cache-persist' import { MMKVWrapper } from 'apollo3-cache-persist'
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
import { default as React, StrictMode, useCallback, useEffect } from 'react' import { PropsWithChildren, default as React, StrictMode, useCallback, useEffect } from 'react'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { LogBox, NativeModules, StatusBar } from 'react-native' import { LogBox, NativeModules, StatusBar } from 'react-native'
import appsFlyer from 'react-native-appsflyer' import appsFlyer from 'react-native-appsflyer'
...@@ -15,7 +21,6 @@ import { SafeAreaProvider } from 'react-native-safe-area-context' ...@@ -15,7 +21,6 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens' import { enableFreeze } from 'react-native-screens'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { DatadogProviderWrapper } from 'src/app/DataDogProvider'
import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider'
import { AppModals } from 'src/app/modals/AppModals' import { AppModals } from 'src/app/modals/AppModals'
import { NavigationContainer } from 'src/app/navigation/NavigationContainer' import { NavigationContainer } from 'src/app/navigation/NavigationContainer'
...@@ -40,9 +45,10 @@ import { ...@@ -40,9 +45,10 @@ import {
setI18NUserDefaults, setI18NUserDefaults,
} from 'src/features/widgets/widgets' } from 'src/features/widgets/widgets'
import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' import { useAppStateTrigger } from 'src/utils/useAppStateTrigger'
import { getStatsigEnvironmentTier } from 'src/utils/version' import { getDatadogEnvironment, getStatsigEnvironmentTier } from 'src/utils/version'
import { flexStyles, useIsDarkMode } from 'ui/src' import { flexStyles, useIsDarkMode } from 'ui/src'
import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner' import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner'
import { config } from 'uniswap/src/config'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors' import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors'
...@@ -64,7 +70,7 @@ import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/conte ...@@ -64,7 +70,7 @@ import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/conte
import i18n from 'uniswap/src/i18n/i18n' import i18n from 'uniswap/src/i18n/i18n'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { getUniqueId } from 'utilities/src/device/getUniqueId' import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { isDetoxBuild } from 'utilities/src/environment/constants' import { isDetoxBuild, isJestRun } from 'utilities/src/environment/constants'
import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/Datadog' import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/Datadog'
import { registerConsoleOverrides } from 'utilities/src/logger/console' import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
...@@ -93,6 +99,29 @@ if (__DEV__) { ...@@ -93,6 +99,29 @@ if (__DEV__) {
// Keep the splash screen visible while we fetch resources until one of our landing pages loads // Keep the splash screen visible while we fetch resources until one of our landing pages loads
SplashScreen.preventAutoHideAsync().catch(() => undefined) 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 // 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. // the bottom of the screen and cause tests to fail.
if (isDetoxBuild) { if (isDetoxBuild) {
...@@ -171,6 +200,15 @@ function App(): JSX.Element | null { ...@@ -171,6 +200,15 @@ 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 const MAX_CACHE_SIZE_IN_BYTES = 1024 * 1024 * 25 // 25 MB
// Ensures redux state is available inside usePersistedApolloClient for the custom endpoint // 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 { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native' import { createNavigationContainerRef, NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack' import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { TransitionPresets, createStackNavigator } from '@react-navigation/stack' import { createStackNavigator, TransitionPresets } from '@react-navigation/stack'
import React, { useEffect } from 'react' import React from 'react'
import { DevSettings } from 'react-native'
import { useSelector } from 'react-redux' 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 { renderHeaderBackButton, renderHeaderBackImage } from 'src/app/navigation/components'
import { import {
AppStackParamList, AppStackParamList,
...@@ -14,7 +11,6 @@ import { ...@@ -14,7 +11,6 @@ import {
FiatOnRampStackParamList, FiatOnRampStackParamList,
OnboardingStackParamList, OnboardingStackParamList,
SettingsStackParamList, SettingsStackParamList,
useAppStackNavigation,
} from 'src/app/navigation/types' } from 'src/app/navigation/types'
import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget' import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget'
import { useBiometricCheck } from 'src/features/biometrics/useBiometricCheck' import { useBiometricCheck } from 'src/features/biometrics/useBiometricCheck'
...@@ -79,7 +75,6 @@ import { ...@@ -79,7 +75,6 @@ import {
UnitagScreens, UnitagScreens,
UnitagStackParamList, UnitagStackParamList,
} from 'uniswap/src/types/screens/mobile' } from 'uniswap/src/types/screens/mobile'
import { isDevEnv } from 'utilities/src/environment/env'
import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext' import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors'
...@@ -337,20 +332,6 @@ export function UnitagStackNavigator(): JSX.Element { ...@@ -337,20 +332,6 @@ export function UnitagStackNavigator(): JSX.Element {
export function AppStackNavigator(): JSX.Element { export function AppStackNavigator(): JSX.Element {
const finishedOnboarding = useSelector(selectFinishedOnboarding) const finishedOnboarding = useSelector(selectFinishedOnboarding)
useBiometricCheck() 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 ( return (
<AppStack.Navigator <AppStack.Navigator
...@@ -381,7 +362,6 @@ export function AppStackNavigator(): JSX.Element { ...@@ -381,7 +362,6 @@ export function AppStackNavigator(): JSX.Element {
<AppStack.Group screenOptions={navOptions.presentationModal}> <AppStack.Group screenOptions={navOptions.presentationModal}>
<AppStack.Screen component={EducationScreen} name={MobileScreens.Education} /> <AppStack.Screen component={EducationScreen} name={MobileScreens.Education} />
</AppStack.Group> </AppStack.Group>
{isDevEnv() && <AppStack.Screen component={StorybookUIRoot} name={MobileScreens.Storybook} />}
</AppStack.Navigator> </AppStack.Navigator>
) )
} }
......
...@@ -120,7 +120,6 @@ export type AppStackParamList = { ...@@ -120,7 +120,6 @@ export type AppStackParamList = {
address: string address: string
} }
[MobileScreens.WebView]: { headerTitle: string; uriLink: string } [MobileScreens.WebView]: { headerTitle: string; uriLink: string }
[MobileScreens.Storybook]: undefined
} }
export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList> export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList>
......
...@@ -44,15 +44,12 @@ const dataDogReduxEnhancer = createDatadogReduxEnhancer({ ...@@ -44,15 +44,12 @@ const dataDogReduxEnhancer = createDatadogReduxEnhancer({
}, },
}) })
const enhancers = [dataDogReduxEnhancer] const middlewares: Middleware[] = [getFiatOnRampAggregatorApi().middleware]
if (isNonJestDev) { if (isNonJestDev) {
const reactotron = require('src/../ReactotronConfig').default const createDebugger = require('redux-flipper').default
enhancers.push(reactotron.createEnhancer()) middlewares.push(createDebugger())
} }
const middlewares: Middleware[] = [getFiatOnRampAggregatorApi().middleware]
export const setupStore = ( export const setupStore = (
preloadedState?: PreloadedState<MobileState>, preloadedState?: PreloadedState<MobileState>,
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
...@@ -62,7 +59,7 @@ export const setupStore = ( ...@@ -62,7 +59,7 @@ export const setupStore = (
preloadedState, preloadedState,
additionalSagas: [rootMobileSaga], additionalSagas: [rootMobileSaga],
middlewareAfter: [...middlewares], middlewareAfter: [...middlewares],
enhancers, enhancers: [dataDogReduxEnhancer],
}) })
} }
export const store = setupStore() export const store = setupStore()
......
...@@ -2,9 +2,9 @@ import React from 'react' ...@@ -2,9 +2,9 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { closeAllModals } from 'src/features/modals/modalSlice' import { closeAllModals, closeModal } from 'src/features/modals/modalSlice'
import { Button, Flex, Text, useSporeColors } from 'ui/src' import { Button, Flex, Text, useSporeColors } from 'ui/src'
import { WalletFilled } from 'ui/src/components/icons' import LockIcon from 'ui/src/assets/icons/lock.svg'
import { iconSizes, opacify } from 'ui/src/theme' import { iconSizes, opacify } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal' import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
...@@ -17,6 +17,10 @@ export function RestoreWalletModal(): JSX.Element | null { ...@@ -17,6 +17,10 @@ export function RestoreWalletModal(): JSX.Element | null {
const colors = useSporeColors() const colors = useSporeColors()
const dispatch = useDispatch() const dispatch = useDispatch()
const onDismiss = (): void => {
dispatch(closeModal({ name: ModalName.RestoreWallet }))
}
const onRestore = (): void => { const onRestore = (): void => {
dispatch(closeAllModals()) dispatch(closeAllModals())
navigate(MobileScreens.OnboardingStack, { navigate(MobileScreens.OnboardingStack, {
...@@ -29,7 +33,7 @@ export function RestoreWalletModal(): JSX.Element | null { ...@@ -29,7 +33,7 @@ export function RestoreWalletModal(): JSX.Element | null {
} }
return ( return (
<Modal hideHandlebar backgroundColor={colors.surface2.val} isDismissible={false} name={ModalName.RestoreWallet}> <Modal backgroundColor={colors.surface2.val} isDismissible={false} name={ModalName.RestoreWallet}>
<Flex centered gap="$spacing16" px="$spacing24" py="$spacing12"> <Flex centered gap="$spacing16" px="$spacing24" py="$spacing12">
<Flex <Flex
centered centered
...@@ -39,7 +43,7 @@ export function RestoreWalletModal(): JSX.Element | null { ...@@ -39,7 +43,7 @@ export function RestoreWalletModal(): JSX.Element | null {
backgroundColor: opacify(12, colors.neutral1.val), backgroundColor: opacify(12, colors.neutral1.val),
}} }}
> >
<WalletFilled color="$neutral1" size={iconSizes.icon24} /> <LockIcon color={colors.neutral1.get()} height={iconSizes.icon24} width={iconSizes.icon24} />
</Flex> </Flex>
<Text textAlign="center" variant="body1"> <Text textAlign="center" variant="body1">
{t('account.wallet.button.restore')} {t('account.wallet.button.restore')}
...@@ -48,6 +52,9 @@ export function RestoreWalletModal(): JSX.Element | null { ...@@ -48,6 +52,9 @@ export function RestoreWalletModal(): JSX.Element | null {
{t('account.wallet.restore.description')} {t('account.wallet.restore.description')}
</Text> </Text>
<Flex centered row gap="$spacing12" pt="$spacing12"> <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}> <Button fill testID={TestID.RestoreWallet} theme="primary" onPress={onRestore}>
{t('common.button.restore')} {t('common.button.restore')}
</Button> </Button>
......
import type { Meta, StoryObj } from '@storybook/react' import { ComponentMeta, ComponentStory } from '@storybook/react'
import React from 'react'
import { CopyTextButton } from 'src/components/buttons/CopyTextButton' import { CopyTextButton } from 'src/components/buttons/CopyTextButton'
import { StorybookTitles } from 'ui/src/storybook'
const meta = { export default {
title: StorybookTitles.Atoms, title: 'WIP/Button/Copy',
component: CopyTextButton, component: CopyTextButton,
} satisfies Meta<typeof CopyTextButton> } as ComponentMeta<typeof CopyTextButton>
type Story = StoryObj<typeof meta> const Template: ComponentStory<typeof CopyTextButton> = (args) => <CopyTextButton {...args} />
const CopyTextButtonStory: Story = { export const Primary = Template.bind({})
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 // 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_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, Platform } from 'react-native' import { Alert, Platform } 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 { call, delay, put, select, takeLatest } from 'typed-redux-saga' 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 { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { finalizeTransaction } from 'uniswap/src/features/transactions/slice' import { finalizeTransaction } from 'uniswap/src/features/transactions/slice'
...@@ -10,15 +12,19 @@ import { openUri } from 'uniswap/src/utils/linking' ...@@ -10,15 +12,19 @@ import { openUri } from 'uniswap/src/utils/linking'
import { isJestRun } from 'utilities/src/environment/constants' import { isJestRun } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { isAndroid } from 'utilities/src/platform' 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'
function isAndroid14(): boolean { function isAndroid14(): boolean {
return isAndroid && Platform.Version === 34 return isAndroid && Platform.Version === 34
} }
import { ONE_DAY_MS, ONE_SECOND_MS } from 'utilities/src/time/time'
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
// small delay to help ux // small delay to help ux
const SWAP_FINALIZED_PROMPT_DELAY_MS = 3 * ONE_SECOND_MS const SWAP_FINALIZED_PROMPT_DELAY_MS = 3 * ONE_SECOND_MS
...@@ -72,8 +78,24 @@ function* maybeRequestAppRating() { ...@@ -72,8 +78,24 @@ function* maybeRequestAppRating() {
return return
} }
const { shouldPrompt, appRatingProvidedMs, appRatingPromptedMs, consecutiveSwapsCondition } = // Conditions
yield* select(appRatingStateSelector) 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)
if (!shouldPrompt) { if (!shouldPrompt) {
logger.debug('appRating', 'maybeRequestAppRating', 'Skipping app rating', { logger.debug('appRating', 'maybeRequestAppRating', 'Skipping app rating', {
...@@ -136,9 +158,9 @@ function* maybeRequestAppRating() { ...@@ -136,9 +158,9 @@ function* maybeRequestAppRating() {
*/ */
async function openRatingOptionsAlert() { async function openRatingOptionsAlert() {
return new Promise((resolve) => { return new Promise((resolve) => {
Alert.alert(i18n.t('appRating.mobile.title'), i18n.t('appRating.description'), [ Alert.alert(i18n.t('mobile.appRating.title'), i18n.t('mobile.appRating.description'), [
{ {
text: i18n.t('appRating.button.notReally'), text: i18n.t('mobile.appRating.button.decline'),
onPress: () => resolve(false), onPress: () => resolve(false),
style: 'cancel', style: 'cancel',
}, },
...@@ -161,11 +183,11 @@ async function openRatingOptionsAlert() { ...@@ -161,11 +183,11 @@ async function openRatingOptionsAlert() {
/** Opens feedback request modal which will redirect to our feedback form. */ /** Opens feedback request modal which will redirect to our feedback form. */
async function openFeedbackRequestAlert() { async function openFeedbackRequestAlert() {
return new Promise((resolve) => { return new Promise((resolve) => {
Alert.alert(i18n.t('appRating.feedback.title'), i18n.t('appRating.feedback.description'), [ Alert.alert(i18n.t('mobile.appRating.feedback.title'), i18n.t('mobile.appRating.feedback.description'), [
{ {
text: i18n.t('appRating.feedback.button.send'), text: i18n.t('mobile.appRating.feedback.button.send'),
onPress: () => { onPress: () => {
openUri(uniswapUrls.walletFeedbackForm).catch((e) => openUri(APP_FEEDBACK_LINK).catch((e) =>
logger.error(e, { tags: { file: 'appRating/saga', function: 'openFeedbackAlert' } }), logger.error(e, { tags: { file: 'appRating/saga', function: 'openFeedbackAlert' } }),
) )
resolve(true) resolve(true)
...@@ -173,7 +195,7 @@ async function openFeedbackRequestAlert() { ...@@ -173,7 +195,7 @@ async function openFeedbackRequestAlert() {
isPreferred: true, isPreferred: true,
}, },
{ {
text: i18n.t('common.button.later'), text: i18n.t('mobile.appRating.feedback.button.cancel'),
onPress: () => resolve(false), onPress: () => resolve(false),
style: 'cancel', style: 'cancel',
}, },
......
import { MobileState } from 'src/app/mobileReducer'
import { hasConsecutiveRecentSwapsSelector } from 'src/features/appRating/selectors'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { import {
TransactionDetails, TransactionDetails,
...@@ -5,13 +7,6 @@ import { ...@@ -5,13 +7,6 @@ import {
TransactionType, TransactionType,
} from 'uniswap/src/features/transactions/types/transactionDetails' } from 'uniswap/src/features/transactions/types/transactionDetails'
import { ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time' import { ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time'
import {
MIN_FEEDBACK_REMINDER_MS,
MIN_PROMPT_REMINDER_MS,
appRatingStateSelector,
hasConsecutiveRecentSwapsSelector,
} from 'wallet/src/features/appRating/selectors'
import { WalletState } from 'wallet/src/state/walletReducer'
import { signerMnemonicAccount } from 'wallet/src/test/fixtures' import { signerMnemonicAccount } from 'wallet/src/test/fixtures'
import { preloadedWalletReducerState } from 'wallet/src/test/fixtures/wallet/redux' import { preloadedWalletReducerState } from 'wallet/src/test/fixtures/wallet/redux'
...@@ -45,7 +40,7 @@ const state = { ...@@ -45,7 +40,7 @@ const state = {
}, },
}, },
}, },
} as unknown as WalletState } as unknown as MobileState
describe('consecutiveSwapsSelector', () => { describe('consecutiveSwapsSelector', () => {
it('returns false for empty state', () => { it('returns false for empty state', () => {
...@@ -61,7 +56,7 @@ describe('consecutiveSwapsSelector', () => { ...@@ -61,7 +56,7 @@ describe('consecutiveSwapsSelector', () => {
const condition = hasConsecutiveRecentSwapsSelector({ const condition = hasConsecutiveRecentSwapsSelector({
...state, ...state,
wallet: { appRatingPromptedMs: MOCK_DATE_PROMPTED + 2000 }, wallet: { appRatingPromptedMs: MOCK_DATE_PROMPTED + 2000 },
} as unknown as WalletState) } as unknown as MobileState)
expect(condition).toBeFalsy() expect(condition).toBeFalsy()
}) })
...@@ -149,139 +144,3 @@ describe('consecutiveSwapsSelector', () => { ...@@ -149,139 +144,3 @@ describe('consecutiveSwapsSelector', () => {
expect(isConsecutiveSwaps).toBeTruthy() expect(isConsecutiveSwaps).toBeTruthy()
}) })
}) })
describe('appRatingStateSelector', () => {
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => MOCK_DATE_PROMPTED)
})
afterEach(() => {
jest.restoreAllMocks()
})
it('returns correct state when never prompted before', () => {
const baseState = {
...state,
wallet: {
appRatingProvidedMs: undefined,
appRatingPromptedMs: undefined,
appRatingFeedbackProvidedMs: undefined,
},
} as WalletState
const result = appRatingStateSelector(baseState)
expect(result).toEqual({
appRatingPromptedMs: undefined,
appRatingProvidedMs: undefined,
consecutiveSwapsCondition: true,
shouldPrompt: true,
})
})
it('returns shouldPrompt false when consecutive swaps condition is false', () => {
const baseState = {
...state,
wallet: {
appRatingProvidedMs: undefined,
appRatingPromptedMs: undefined,
appRatingFeedbackProvidedMs: undefined,
},
transactions: {},
} as WalletState
const result = appRatingStateSelector(baseState)
expect(result).toEqual({
appRatingPromptedMs: undefined,
appRatingProvidedMs: undefined,
consecutiveSwapsCondition: false,
shouldPrompt: false,
})
})
it('returns shouldPrompt true when enough time passed since last prompt', () => {
const lastPromptTime = MOCK_DATE_PROMPTED - MIN_PROMPT_REMINDER_MS - ONE_HOUR_MS
const baseState = {
...state,
wallet: {
appRatingProvidedMs: undefined,
appRatingPromptedMs: lastPromptTime,
appRatingFeedbackProvidedMs: undefined,
},
} as WalletState
const result = appRatingStateSelector(baseState)
expect(result).toEqual({
appRatingPromptedMs: lastPromptTime,
appRatingProvidedMs: undefined,
consecutiveSwapsCondition: true,
shouldPrompt: true,
})
})
it('returns shouldPrompt false when not enough time passed since last prompt', () => {
const lastPromptTime = MOCK_DATE_PROMPTED - MIN_PROMPT_REMINDER_MS + ONE_HOUR_MS
const baseState = {
...state,
wallet: {
appRatingProvidedMs: undefined,
appRatingPromptedMs: lastPromptTime,
appRatingFeedbackProvidedMs: undefined,
},
} as WalletState
const result = appRatingStateSelector(baseState)
expect(result).toEqual({
appRatingPromptedMs: lastPromptTime,
appRatingProvidedMs: undefined,
consecutiveSwapsCondition: true,
shouldPrompt: false,
})
})
it('returns shouldPrompt true when enough time passed since last feedback', () => {
const lastFeedbackTime = MOCK_DATE_PROMPTED - MIN_FEEDBACK_REMINDER_MS - ONE_HOUR_MS
const baseState = {
...state,
wallet: {
appRatingProvidedMs: undefined,
appRatingPromptedMs: undefined,
appRatingFeedbackProvidedMs: lastFeedbackTime,
},
} as WalletState
const result = appRatingStateSelector(baseState)
expect(result).toEqual({
appRatingPromptedMs: undefined,
appRatingProvidedMs: undefined,
consecutiveSwapsCondition: true,
shouldPrompt: true,
})
})
it('returns shouldPrompt false when not enough time passed since last feedback', () => {
const lastPromptTime = MOCK_DATE_PROMPTED - MIN_PROMPT_REMINDER_MS - ONE_HOUR_MS
const lastFeedbackTime = MOCK_DATE_PROMPTED - MIN_FEEDBACK_REMINDER_MS + ONE_HOUR_MS
const baseState = {
...state,
wallet: {
appRatingProvidedMs: undefined,
appRatingPromptedMs: lastPromptTime,
appRatingFeedbackProvidedMs: lastFeedbackTime,
},
} as WalletState
const result = appRatingStateSelector(baseState)
expect(result).toEqual({
appRatingPromptedMs: lastPromptTime,
appRatingProvidedMs: undefined,
consecutiveSwapsCondition: true,
shouldPrompt: false,
})
})
})
import { createSelector, Selector } from '@reduxjs/toolkit' import { createSelector, Selector } from '@reduxjs/toolkit'
import { MobileState } from 'src/app/mobileReducer'
import { selectTransactions } from 'uniswap/src/features/transactions/selectors' import { selectTransactions } from 'uniswap/src/features/transactions/selectors'
import { TransactionsState } from 'uniswap/src/features/transactions/slice' import { TransactionsState } from 'uniswap/src/features/transactions/slice'
import { import {
...@@ -7,22 +8,12 @@ import { ...@@ -7,22 +8,12 @@ import {
TransactionType, TransactionType,
} from 'uniswap/src/features/transactions/types/transactionDetails' } from 'uniswap/src/features/transactions/types/transactionDetails'
import { flattenObjectOfObjects } from 'utilities/src/primitives/objects' import { flattenObjectOfObjects } from 'utilities/src/primitives/objects'
import { ONE_DAY_MS, ONE_MINUTE_MS } from 'utilities/src/time/time' import { ONE_MINUTE_MS } from 'utilities/src/time/time'
import {
appRatingFeedbackProvidedMsSelector,
appRatingPromptedMsSelector,
appRatingProvidedMsSelector,
} from 'wallet/src/features/wallet/selectors'
import { WalletState } from 'wallet/src/state/walletReducer'
const NUM_CONSECUTIVE_SWAPS = 2 const NUM_CONSECUTIVE_SWAPS = 2
// at most once per reminder period (120 days)
export const MIN_PROMPT_REMINDER_MS = 120 * ONE_DAY_MS
// remind after a longer delay when user filled the feedback form (180 days)
export const MIN_FEEDBACK_REMINDER_MS = 180 * ONE_DAY_MS
export const hasConsecutiveRecentSwapsSelector: Selector<WalletState, boolean> = createSelector( export const hasConsecutiveRecentSwapsSelector: Selector<MobileState, boolean> = createSelector(
[selectTransactions, (state: WalletState): number => state.wallet.appRatingPromptedMs ?? 0], [selectTransactions, (state: MobileState): number => state.wallet.appRatingPromptedMs ?? 0],
(transactions: TransactionsState, appRatingPromptedMs): boolean => { (transactions: TransactionsState, appRatingPromptedMs): boolean => {
const swapTxs: Array<TransactionDetails> = [] const swapTxs: Array<TransactionDetails> = []
...@@ -40,10 +31,8 @@ export const hasConsecutiveRecentSwapsSelector: Selector<WalletState, boolean> = ...@@ -40,10 +31,8 @@ export const hasConsecutiveRecentSwapsSelector: Selector<WalletState, boolean> =
} }
} }
// Sort transactions by time, most recent first const recentSwaps = swapTxs.slice(-NUM_CONSECUTIVE_SWAPS)
const sortedSwaps = [...swapTxs].sort((a, b) => b.addedTime - a.addedTime) const mostRecentSwapTime = recentSwaps[recentSwaps.length - 1]?.addedTime
const recentSwaps = sortedSwaps.slice(0, NUM_CONSECUTIVE_SWAPS)
const mostRecentSwapTime = recentSwaps[0]?.addedTime
const mostRecentSwapLessThanMinAgo = Boolean(mostRecentSwapTime && Date.now() - mostRecentSwapTime < ONE_MINUTE_MS) const mostRecentSwapLessThanMinAgo = Boolean(mostRecentSwapTime && Date.now() - mostRecentSwapTime < ONE_MINUTE_MS)
return ( return (
...@@ -53,54 +42,3 @@ export const hasConsecutiveRecentSwapsSelector: Selector<WalletState, boolean> = ...@@ -53,54 +42,3 @@ export const hasConsecutiveRecentSwapsSelector: Selector<WalletState, boolean> =
) )
}, },
) )
/**
* Selector that determines if and when to show the app rating prompt.
* The prompt should be shown when ALL of these conditions are met:
* 1. User has completed consecutive successful swaps recently (consecutiveSwapsCondition)
* 2. Either:
* a. User has never been prompted before (hasNeverPrompted), OR
* b. Enough time has passed since the last interaction:
* - If user never provided feedback: 120 days since last prompt
* - If user provided feedback: 180 days since feedback was given
*
* Returns state including prompt timing info and whether prompt should be shown.
*/
export const appRatingStateSelector: Selector<
WalletState,
{
appRatingProvidedMs: number | undefined
appRatingPromptedMs: number | undefined
consecutiveSwapsCondition: boolean
shouldPrompt: boolean
}
> = createSelector(
[
appRatingProvidedMsSelector,
appRatingPromptedMsSelector,
appRatingFeedbackProvidedMsSelector,
hasConsecutiveRecentSwapsSelector,
],
(appRatingProvidedMs, appRatingPromptedMs, appRatingFeedbackProvidedMs, consecutiveSwapsCondition) => {
const hasPrompted = appRatingPromptedMs !== undefined
const hasProvidedFeedback = appRatingFeedbackProvidedMs !== undefined
// Check if enough time has passed since last prompt (when no feedback given)
const hasPassedPromptDelay =
!hasProvidedFeedback && hasPrompted && Date.now() - appRatingPromptedMs > MIN_PROMPT_REMINDER_MS
// Check if enough time has passed since last feedback
const hasPassedFeedbackDelay =
hasProvidedFeedback && Date.now() - appRatingFeedbackProvidedMs > MIN_FEEDBACK_REMINDER_MS
// Determine if we should show reminder based on all timing conditions
const reminderCondition = !hasPrompted || hasPassedPromptDelay || hasPassedFeedbackDelay
return {
appRatingPromptedMs,
appRatingProvidedMs,
consecutiveSwapsCondition,
shouldPrompt: consecutiveSwapsCondition && reminderCondition,
}
},
)
...@@ -271,7 +271,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi ...@@ -271,7 +271,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
))} ))}
</Flex> </Flex>
) : ( ) : (
<TouchableArea disabled={selectTokenLoading} onPress={onToggleIsTokenInputMode}> <TouchableArea onPress={onToggleIsTokenInputMode}>
<Flex <Flex
centered centered
row row
......
...@@ -159,26 +159,19 @@ export const parseTransactionRequest = ( ...@@ -159,26 +159,19 @@ export const parseTransactionRequest = (
} }
} }
function isUtf8(str: string): boolean {
try {
const decoded = new TextDecoder('utf-8').decode(new TextEncoder().encode(str))
// if the encoded -> decoded string matches the original string (ie no chars swapped),
// then it's valid utf-8
return decoded === str
} catch {
return false
}
}
export function decodeMessage(value: string): string { export function decodeMessage(value: string): string {
if (utils.isHexString(value) && isUtf8(value)) { try {
if (utils.isHexString(value)) {
const decoded = utils.toUtf8String(value) const decoded = utils.toUtf8String(value)
if (decoded?.trim()) { if (decoded?.trim()) {
return decoded return decoded
} }
} }
return value return value
} catch {
return value
}
} }
/** /**
......
...@@ -13,6 +13,8 @@ LogBox.ignoreLogs([ ...@@ -13,6 +13,8 @@ LogBox.ignoreLogs([
'logException:ApolloClient [Network Error]:', 'logException:ApolloClient [Network Error]:',
// Ignore since it's difficult to filter out just these styles and they are often shared styles // 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.', '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 // 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.', '[Reanimated] Reduced motion setting is enabled on this device.',
]) ])
...@@ -103,27 +103,3 @@ require('@formatjs/intl-relativetimeformat/locale-data/tr').default ...@@ -103,27 +103,3 @@ require('@formatjs/intl-relativetimeformat/locale-data/tr').default
require('@formatjs/intl-relativetimeformat/locale-data/uk').default require('@formatjs/intl-relativetimeformat/locale-data/uk').default
require('@formatjs/intl-relativetimeformat/locale-data/ur').default require('@formatjs/intl-relativetimeformat/locale-data/ur').default
require('@formatjs/intl-relativetimeformat/locale-data/vi').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,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
} from 'src/components/Settings/SettingsRow' } from 'src/components/Settings/SettingsRow'
import { WalletSettings } from 'src/components/Settings/WalletSettings' import { WalletSettings } from 'src/components/Settings/WalletSettings'
import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen' import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen'
import { APP_FEEDBACK_LINK } from 'src/constants/urls'
import { useBiometricContext } from 'src/features/biometrics/context' import { useBiometricContext } from 'src/features/biometrics/context'
import { useBiometricName, useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks' import { useBiometricName, useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks'
import { useWalletRestore } from 'src/features/wallet/hooks' import { useWalletRestore } from 'src/features/wallet/hooks'
...@@ -262,7 +263,7 @@ export function SettingsScreen(): JSX.Element { ...@@ -262,7 +263,7 @@ export function SettingsScreen(): JSX.Element {
{ {
screen: MobileScreens.WebView, screen: MobileScreens.WebView,
screenProps: { screenProps: {
uriLink: uniswapUrls.walletFeedbackForm, uriLink: APP_FEEDBACK_LINK,
headerTitle: t('settings.action.feedback'), headerTitle: t('settings.action.feedback'),
}, },
text: 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,7 +2,6 @@ ...@@ -2,7 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"display": "Mobile App", "display": "Mobile App",
"extends": "../../config/tsconfig/expo.json", "extends": "../../config/tsconfig/expo.json",
"exclude": [".storybook/storybook.requires.ts"],
"references": [ "references": [
{ {
"path": "../../packages/ui" "path": "../../packages/ui"
......
...@@ -120,7 +120,7 @@ ...@@ -120,7 +120,7 @@
"eslint": "8.44.0", "eslint": "8.44.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-rulesdir": "0.2.2", "eslint-plugin-rulesdir": "0.2.2",
"hardhat": "2.22.16", "hardhat": "2.14.0",
"husky": "8.0.3", "husky": "8.0.3",
"jest": "29.7.0", "jest": "29.7.0",
"jest-extended": "4.0.1", "jest-extended": "4.0.1",
...@@ -181,26 +181,26 @@ ...@@ -181,26 +181,26 @@
"@types/react-scroll-sync": "0.8.7", "@types/react-scroll-sync": "0.8.7",
"@types/react-window-infinite-loader": "1.0.6", "@types/react-window-infinite-loader": "1.0.6",
"@uniswap/analytics": "1.7.0", "@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.40.0", "@uniswap/analytics-events": "2.39.0",
"@uniswap/client-explore": "0.0.12", "@uniswap/client-explore": "0.0.12",
"@uniswap/client-pools": "0.0.9", "@uniswap/client-pools": "0.0.9",
"@uniswap/liquidity-staker": "1.0.2", "@uniswap/liquidity-staker": "1.0.2",
"@uniswap/merkle-distributor": "1.0.1", "@uniswap/merkle-distributor": "1.0.1",
"@uniswap/permit2-sdk": "1.3.0", "@uniswap/permit2-sdk": "1.3.0",
"@uniswap/redux-multicall": "1.1.8", "@uniswap/redux-multicall": "1.1.8",
"@uniswap/router-sdk": "1.15.0", "@uniswap/router-sdk": "1.14.3",
"@uniswap/sdk-core": "6.0.0", "@uniswap/sdk-core": "5.9.0",
"@uniswap/smart-order-router": "3.17.3", "@uniswap/smart-order-router": "3.17.3",
"@uniswap/token-lists": "1.0.0-beta.33", "@uniswap/token-lists": "1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18", "@uniswap/uniswapx-sdk": "2.1.0-beta.18",
"@uniswap/universal-router-sdk": "4.7.0", "@uniswap/universal-router-sdk": "4.5.2",
"@uniswap/v2-core": "1.0.1", "@uniswap/v2-core": "1.0.1",
"@uniswap/v2-periphery": "1.1.0-beta.0", "@uniswap/v2-periphery": "1.1.0-beta.0",
"@uniswap/v2-sdk": "4.7.0", "@uniswap/v2-sdk": "4.6.1",
"@uniswap/v3-core": "1.0.1", "@uniswap/v3-core": "1.0.1",
"@uniswap/v3-periphery": "1.4.4", "@uniswap/v3-periphery": "1.4.4",
"@uniswap/v3-sdk": "3.19.0", "@uniswap/v3-sdk": "3.18.1",
"@uniswap/v4-sdk": "1.12.0", "@uniswap/v4-sdk": "1.10.3",
"@vanilla-extract/css": "1.14.0", "@vanilla-extract/css": "1.14.0",
"@vanilla-extract/dynamic": "2.1.0", "@vanilla-extract/dynamic": "2.1.0",
"@vanilla-extract/sprinkles": "1.6.1", "@vanilla-extract/sprinkles": "1.6.1",
...@@ -256,6 +256,7 @@ ...@@ -256,6 +256,7 @@
"react-redux": "8.0.5", "react-redux": "8.0.5",
"react-router-dom": "6.10.0", "react-router-dom": "6.10.0",
"react-scroll-sync": "0.11.2", "react-scroll-sync": "0.11.2",
"react-spring": "9.7.3",
"react-table": "7.8.0", "react-table": "7.8.0",
"react-use-gesture": "6.0.14", "react-use-gesture": "6.0.14",
"react-virtualized-auto-sizer": "1.0.20", "react-virtualized-auto-sizer": "1.0.20",
......
...@@ -126,4 +126,16 @@ ...@@ -126,4 +126,16 @@
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.5</priority> <priority>0.5</priority>
</url> </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> </urlset>
...@@ -53,7 +53,6 @@ ...@@ -53,7 +53,6 @@
"https://cdn.center.app/", "https://cdn.center.app/",
"https://celo-org.github.io", "https://celo-org.github.io",
"https://cloudflare-eth.com", "https://cloudflare-eth.com",
"https://erasld2vrf.execute-api.us-east-2.amazonaws.com",
"https://ethereum-optimism.github.io/", "https://ethereum-optimism.github.io/",
"https://forno.celo.org/", "https://forno.celo.org/",
"https://gateway.ipfs.io/", "https://gateway.ipfs.io/",
...@@ -94,9 +93,7 @@ ...@@ -94,9 +93,7 @@
"https://vercel.live/", "https://vercel.live/",
"https://wallet.crypto.com", "https://wallet.crypto.com",
"https://web3.1inch.io", "https://web3.1inch.io",
"https://x6ahx1oagk.execute-api.us-east-2.amazonaws.com",
"https://mainnet.era.zksync.io/", "https://mainnet.era.zksync.io/",
"https://8mr3mthjba.execute-api.us-east-2.amazonaws.com",
"wss://*.uniswap.org", "wss://*.uniswap.org",
"wss://relay.walletconnect.com", "wss://relay.walletconnect.com",
"wss://relay.walletconnect.org", "wss://relay.walletconnect.org",
......
...@@ -83,17 +83,6 @@ ...@@ -83,17 +83,6 @@
padding: 0; 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 { button {
user-select: none; 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 ...@@ -234,9 +234,9 @@ function CreateV3PoolSummary({ info: { quoteCurrencyId, baseCurrencyId } }: { in
) )
} }
function CollectFeesSummary({ info: { token0CurrencyId, token1CurrencyId } }: { info: CollectFeesTransactionInfo }) { function CollectFeesSummary({ info: { currencyId0, currencyId1 } }: { info: CollectFeesTransactionInfo }) {
const currency0 = useCurrency(token0CurrencyId) const currency0 = useCurrency(currencyId0)
const currency1 = useCurrency(token1CurrencyId) const currency1 = useCurrency(currencyId1)
return ( return (
<Trans <Trans
......
...@@ -3,23 +3,27 @@ import { ConfirmedIcon, LogoContainer, SubmittedIcon } from 'components/AccountD ...@@ -3,23 +3,27 @@ import { ConfirmedIcon, LogoContainer, SubmittedIcon } from 'components/AccountD
import { useCancelOrdersGasEstimate } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks' import { useCancelOrdersGasEstimate } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks'
import { Container, Dialog, DialogButtonType, DialogProps } from 'components/Dialog/Dialog' import { Container, Dialog, DialogButtonType, DialogProps } from 'components/Dialog/Dialog'
import { LoaderV3 } from 'components/Icons/LoadingSpinner' import { LoaderV3 } from 'components/Icons/LoadingSpinner'
import Modal from 'components/Modal'
import { GetHelpHeader } from 'components/Modal/GetHelpHeader' import { GetHelpHeader } from 'components/Modal/GetHelpHeader'
import Column from 'components/deprecated/Column'
import Row from 'components/deprecated/Row' import Row from 'components/deprecated/Row'
import { DetailLineItem } from 'components/swap/DetailLineItem' import { DetailLineItem } from 'components/swap/DetailLineItem'
import styled, { useTheme } from 'lib/styled-components' import styled, { useTheme } from 'lib/styled-components'
import { Slash } from 'react-feather' import { Slash } from 'react-feather'
import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types'
import { ExternalLink, ThemedText } from 'theme/components' 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 { nativeOnChain } from 'uniswap/src/constants/tokens'
import { UniverseChainId } from 'uniswap/src/features/chains/types' 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 { useUSDCValue } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice'
import { Plural, Trans, t } from 'uniswap/src/i18n' import { Plural, Trans, t } from 'uniswap/src/i18n'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { NumberType, useFormatter } from 'utils/formatNumbers' 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)` const ModalHeader = styled(GetHelpHeader)`
padding: 4px 0px; padding: 4px 0px;
` `
...@@ -96,7 +100,7 @@ export function CancelOrdersDialog( ...@@ -96,7 +100,7 @@ export function CancelOrdersDialog(
(cancelState === CancellationState.CANCELLED || cancelState === CancellationState.PENDING_CONFIRMATION) && (cancelState === CancellationState.CANCELLED || cancelState === CancellationState.PENDING_CONFIRMATION) &&
cancelTxHash cancelTxHash
return ( return (
<Modal name={ModalName.CancelOrders} isModalOpen onClose={onCancel} maxHeight="90vh" padding={0}> <Modal isOpen $scrollOverlay onDismiss={onCancel} maxHeight="90vh">
<Container gap="lg"> <Container gap="lg">
<ModalHeader closeModal={onCancel} /> <ModalHeader closeModal={onCancel} />
<LogoContainer>{icon}</LogoContainer> <LogoContainer>{icon}</LogoContainer>
...@@ -127,14 +131,14 @@ export function CancelOrdersDialog( ...@@ -127,14 +131,14 @@ export function CancelOrdersDialog(
icon={icon} icon={icon}
title={title} title={title}
description={ description={
<Flex width="100%"> <Column>
<Plural <Plural
value={orders.length} value={orders.length}
one={t('swap.cancel.cannotExecute')} one={t('swap.cancel.cannotExecute')}
other={t('swap.cancel.cannotExecute.plural')} other={t('swap.cancel.cannotExecute.plural')}
/> />
<GasEstimateDisplay chainId={orders[0].chainId} gasEstimateValue={gasEstimate?.value} /> <GasEstimateDisplay chainId={orders[0].chainId} gasEstimateValue={gasEstimate?.value} />
</Flex> </Column>
} }
buttonsConfig={{ buttonsConfig={{
left: { left: {
...@@ -166,15 +170,14 @@ function GasEstimateDisplay({ gasEstimateValue, chainId }: { gasEstimateValue?: ...@@ -166,15 +170,14 @@ function GasEstimateDisplay({ gasEstimateValue, chainId }: { gasEstimateValue?:
amount: gasFeeUSD, amount: gasFeeUSD,
type: NumberType.PortfolioBalance, type: NumberType.PortfolioBalance,
}) })
return ( return (
<Flex row mt={16} pt={16} borderColor="$transparent" borderTopColor="$surface3" borderWidth={1} width="100%"> <GasEstimateContainer>
<DetailLineItem <DetailLineItem
LineItem={{ LineItem={{
Label: () => <Trans i18nKey="common.networkCost" />, Label: () => <Trans i18nKey="common.networkCost" />,
Value: () => <span>{gasEstimateValue ? gasFeeFormatted : '-'}</span>, Value: () => <span>{gasEstimateValue ? gasFeeFormatted : '-'}</span>,
}} }}
/> />
</Flex> </GasEstimateContainer>
) )
} }
...@@ -17,6 +17,7 @@ import { formatTimestamp } from 'components/AccountDrawer/MiniPortfolio/formatTi ...@@ -17,6 +17,7 @@ import { formatTimestamp } from 'components/AccountDrawer/MiniPortfolio/formatTi
import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button/buttons' import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button/buttons'
import { OpacityHoverState } from 'components/Common/styles' import { OpacityHoverState } from 'components/Common/styles'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled' import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import Modal from 'components/Modal'
import Column, { AutoColumn } from 'components/deprecated/Column' import Column, { AutoColumn } from 'components/deprecated/Column'
import Row from 'components/deprecated/Row' import Row from 'components/deprecated/Row'
import { LimitDisclaimer } from 'components/swap/LimitDisclaimer' import { LimitDisclaimer } from 'components/swap/LimitDisclaimer'
...@@ -32,9 +33,8 @@ import { useOrder } from 'state/signatures/hooks' ...@@ -32,9 +33,8 @@ import { useOrder } from 'state/signatures/hooks'
import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types'
import { Divider, ThemedText } from 'theme/components' import { Divider, ThemedText } from 'theme/components'
import { UniswapXOrderStatus } from 'types/uniswapx' import { UniswapXOrderStatus } from 'types/uniswapx'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { InterfaceEventNameLocal, ModalName } from 'uniswap/src/features/telemetry/constants' import { InterfaceEventNameLocal } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { Trans } from 'uniswap/src/i18n' import { Trans } from 'uniswap/src/i18n'
import { CurrencyField } from 'uniswap/src/types/currency' import { CurrencyField } from 'uniswap/src/types/currency'
...@@ -383,11 +383,9 @@ export function OffchainActivityModal() { ...@@ -383,11 +383,9 @@ export function OffchainActivityModal() {
/> />
)} )}
<Modal <Modal
name={ModalName.OffchainActivity}
maxWidth={375} maxWidth={375}
isModalOpen={!!selectedOrderAtomValue?.modalOpen && cancelState === CancellationState.NOT_STARTED} isOpen={!!selectedOrderAtomValue?.modalOpen && cancelState === CancellationState.NOT_STARTED}
onClose={reset} onDismiss={reset}
padding={0}
> >
<Wrapper data-testid="offchain-activity-modal"> <Wrapper data-testid="offchain-activity-modal">
<Row justify="space-between"> <Row justify="space-between">
......
...@@ -3,6 +3,36 @@ ...@@ -3,6 +3,36 @@
exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = ` exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = `
<DocumentFragment> <DocumentFragment>
.c0 { .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; color: #222222;
-webkit-letter-spacing: -0.01em; -webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em;
...@@ -10,17 +40,17 @@ exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = ` ...@@ -10,17 +40,17 @@ exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = `
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
.c1 { .c4 {
cursor: auto; cursor: auto;
color: #7D7D7D; color: #7D7D7D;
} }
.c2 { .c5 {
text-align: right; text-align: right;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.c3 { .c6 {
background-color: transparent; background-color: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
...@@ -63,23 +93,23 @@ exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = ` ...@@ -63,23 +93,23 @@ exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = `
class=" t_light _dsp_contents is_Theme" class=" t_light _dsp_contents is_Theme"
> >
<div <div
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" class="c0 c1 c2"
> >
<div <div
class="c0 c1 css-142zc9n" class="c3 c4 css-142zc9n"
data-testid="swap-li-label" data-testid="swap-li-label"
> >
Rate Rate
</div> </div>
<div <div
class="c0 c2 css-142zc9n" class="c3 c5 css-142zc9n"
> >
<button <button
class="c3" class="c6"
title="1 USDC = <0.00001 DAI " title="1 USDC = <0.00001 DAI "
> >
<div <div
class="c0 css-142zc9n" class="c3 css-142zc9n"
> >
1 USDC = &lt;0.00001 DAI 1 USDC = &lt;0.00001 DAI
</div> </div>
...@@ -96,6 +126,36 @@ exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = ` ...@@ -96,6 +126,36 @@ exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = `
exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = ` exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = `
<DocumentFragment> <DocumentFragment>
.c0 { .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; color: #222222;
-webkit-letter-spacing: -0.01em; -webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em;
...@@ -103,12 +163,12 @@ exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = ` ...@@ -103,12 +163,12 @@ exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = `
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
.c1 { .c4 {
cursor: auto; cursor: auto;
color: #7D7D7D; color: #7D7D7D;
} }
.c2 { .c5 {
text-align: right; text-align: right;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
...@@ -124,16 +184,16 @@ exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = ` ...@@ -124,16 +184,16 @@ exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = `
class=" t_light _dsp_contents is_Theme" class=" t_light _dsp_contents is_Theme"
> >
<div <div
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" class="c0 c1 c2"
> >
<div <div
class="c0 c1 css-142zc9n" class="c3 c4 css-142zc9n"
data-testid="swap-li-label" data-testid="swap-li-label"
> >
Network cost Network cost
</div> </div>
<div <div
class="c0 c2 css-142zc9n" class="c3 c5 css-142zc9n"
> >
<span> <span>
$0 $0
...@@ -149,6 +209,36 @@ exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = ` ...@@ -149,6 +209,36 @@ exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = `
exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = ` exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = `
<DocumentFragment> <DocumentFragment>
.c0 { .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; color: #222222;
-webkit-letter-spacing: -0.01em; -webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em;
...@@ -156,7 +246,7 @@ exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = ` ...@@ -156,7 +246,7 @@ exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = `
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
.c3 { .c6 {
-webkit-text-decoration: none; -webkit-text-decoration: none;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
...@@ -167,20 +257,20 @@ exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = ` ...@@ -167,20 +257,20 @@ exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = `
font-weight: 500; font-weight: 500;
} }
.c3:hover { .c6:hover {
opacity: 0.6; opacity: 0.6;
} }
.c3:active { .c6:active {
opacity: 0.4; opacity: 0.4;
} }
.c1 { .c4 {
cursor: auto; cursor: auto;
color: #7D7D7D; color: #7D7D7D;
} }
.c2 { .c5 {
text-align: right; text-align: right;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
...@@ -196,19 +286,19 @@ exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = ` ...@@ -196,19 +286,19 @@ exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = `
class=" t_light _dsp_contents is_Theme" class=" t_light _dsp_contents is_Theme"
> >
<div <div
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" class="c0 c1 c2"
> >
<div <div
class="c0 c1 css-142zc9n" class="c3 c4 css-142zc9n"
data-testid="swap-li-label" data-testid="swap-li-label"
> >
Transaction ID Transaction ID
</div> </div>
<div <div
class="c0 c2 css-142zc9n" class="c3 c5 css-142zc9n"
> >
<a <a
class="c3" class="c6"
href="https://etherscan.io/tx/0x123" href="https://etherscan.io/tx/0x123"
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
......
...@@ -211,10 +211,10 @@ jest.mock('../../../../state/transactions/hooks', () => { ...@@ -211,10 +211,10 @@ jest.mock('../../../../state/transactions/hooks', () => {
...mockMultiStatus( ...mockMultiStatus(
{ {
type: MockTxType.COLLECT_FEES, type: MockTxType.COLLECT_FEES,
token0CurrencyId: MockUSDC_MAINNET.address, currencyId0: MockUSDC_MAINNET.address,
token1CurrencyId: MockDAI.address, currencyId1: MockDAI.address,
token0CurrencyAmountRaw: mockCurrencyAmountRawUSDC, expectedCurrencyOwed0: mockCurrencyAmountRawUSDC,
token1CurrencyAmountRaw: mockCurrencyAmountRaw, expectedCurrencyOwed1: mockCurrencyAmountRaw,
}, },
'0xcollect_fees', '0xcollect_fees',
), ),
......
...@@ -217,10 +217,10 @@ async function parseCollectFees( ...@@ -217,10 +217,10 @@ async function parseCollectFees(
): Promise<Partial<Activity>> { ): Promise<Partial<Activity>> {
// Adapts CollectFeesTransactionInfo to generic LP type // Adapts CollectFeesTransactionInfo to generic LP type
const { const {
token0CurrencyId: baseCurrencyId, currencyId0: baseCurrencyId,
token1CurrencyId: quoteCurrencyId, currencyId1: quoteCurrencyId,
token0CurrencyAmountRaw: expectedAmountBaseRaw, expectedCurrencyOwed0: expectedAmountBaseRaw,
token1CurrencyAmountRaw: expectedAmountQuoteRaw, expectedCurrencyOwed1: expectedAmountQuoteRaw,
} = collect } = collect
return parseLegacyLP( return parseLegacyLP(
{ baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw }, { baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw },
......
import { InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events' import { InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
import MobileAppLogo from 'assets/svg/uniswap_app_logo.svg' import MobileAppLogo from 'assets/svg/uniswap_app_logo.svg'
import Modal from 'components/Modal'
import { useConnect } from 'hooks/useConnect' import { useConnect } from 'hooks/useConnect'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { CloseIcon } from 'theme/components' import { CloseIcon } from 'theme/components'
import { Button, Flex, Image, QRCodeDisplay, Separator, Text, useSporeColors } from 'ui/src' 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 { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useTranslation } from 'uniswap/src/i18n' import { useTranslation } from 'uniswap/src/i18n'
import { isWebAndroid, isWebIOS } from 'utilities/src/platform' import { isWebAndroid, isWebIOS } from 'utilities/src/platform'
...@@ -49,7 +48,7 @@ export default function UniwalletModal() { ...@@ -49,7 +48,7 @@ export default function UniwalletModal() {
const colors = useSporeColors() const colors = useSporeColors()
return ( return (
<Modal name={ModalName.UniWalletConnect} isModalOpen={open} onClose={close} padding={0}> <Modal isOpen={open} onDismiss={close}>
<Flex shrink grow p="$spacing20"> <Flex shrink grow p="$spacing20">
<Flex row justifyContent="space-between"> <Flex row justifyContent="space-between">
<Text variant="subheading1">{t('account.drawer.modal.scan')}</Text> <Text variant="subheading1">{t('account.drawer.modal.scan')}</Text>
......
...@@ -731,18 +731,8 @@ exports[`AccountDrawer tests AccountDrawer default styles 1`] = ` ...@@ -731,18 +731,8 @@ exports[`AccountDrawer tests AccountDrawer default styles 1`] = `
data-testid="wallet-modal" data-testid="wallet-modal"
> >
<div <div
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column" tabindex="0"
/> />
<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 <div
class="c7 c8 c9" class="c7 c8 c9"
width="100%" width="100%"
......
...@@ -7,13 +7,11 @@ import { useCallback } from 'react' ...@@ -7,13 +7,11 @@ import { useCallback } from 'react'
import { useModalIsOpen, useOpenModal, useToggleModal } from 'state/application/hooks' import { useModalIsOpen, useOpenModal, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer' import { ApplicationModal } from 'state/application/reducer'
import { ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
import { Flex, QRCodeDisplay, Text, useSporeColors } from 'ui/src' import { AdaptiveWebModal, Flex, QRCodeDisplay, Text, useSporeColors } from 'ui/src'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { NetworkLogos } from 'uniswap/src/components/network/NetworkLogos' import { NetworkLogos } from 'uniswap/src/components/network/NetworkLogos'
import { useAddressColorProps } from 'uniswap/src/features/address/color' import { useAddressColorProps } from 'uniswap/src/features/address/color'
import { useOrderedChainIds } from 'uniswap/src/features/chains/hooks/useOrderedChainIds' import { useOrderedChainIds } from 'uniswap/src/features/chains/hooks/useOrderedChainIds'
import { SUPPORTED_CHAIN_IDS } from 'uniswap/src/features/chains/types' 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 { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { Trans } from 'uniswap/src/i18n' import { Trans } from 'uniswap/src/i18n'
...@@ -37,7 +35,7 @@ export function AddressQRModal({ accountAddress }: { accountAddress: Address }) ...@@ -37,7 +35,7 @@ export function AddressQRModal({ accountAddress }: { accountAddress: Address })
}, [toggleModal, openReceiveCryptoModal]) }, [toggleModal, openReceiveCryptoModal])
return ( return (
<Modal isModalOpen={isOpen} onClose={toggleModal} maxWidth={420} name={ModalName.AddressQR}> <AdaptiveWebModal isOpen={isOpen} onClose={toggleModal} width={420}>
<Flex pb="$spacing16" gap="$spacing24"> <Flex pb="$spacing16" gap="$spacing24">
<GetHelpHeader goBack={goBack} closeModal={toggleModal} /> <GetHelpHeader goBack={goBack} closeModal={toggleModal} />
<Flex gap="$spacing12"> <Flex gap="$spacing12">
...@@ -78,6 +76,6 @@ export function AddressQRModal({ accountAddress }: { accountAddress: Address }) ...@@ -78,6 +76,6 @@ export function AddressQRModal({ accountAddress }: { accountAddress: Address })
<NetworkLogos chains={orderedChainIds} /> <NetworkLogos chains={orderedChainIds} />
</Flex> </Flex>
</Flex> </Flex>
</Modal> </AdaptiveWebModal>
) )
} }
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()
})
})
})
import { useRef } from 'react'
import { UseSpringProps, animated, easings, useSpring } from 'react-spring'
import { TRANSITION_DURATIONS } from 'theme/styles'
import useResizeObserver from 'use-resize-observer'
type AnimatedDropdownProps = React.PropsWithChildren<{ open: boolean; springProps?: UseSpringProps }>
/**
* @param open conditional to show content or hide
* @param springProps additional props to include in spring animation
* @returns Wrapper to smoothly hide and expand content
*/
export default function AnimatedDropdown({ open, springProps, children }: AnimatedDropdownProps) {
const wasOpen = useRef(open)
const { ref, height } = useResizeObserver()
const props = useSpring({
// On initial render, `height` will be undefined as ref has not been set yet.
// If the dropdown should be open, we fallback to `auto` to avoid flickering.
// Otherwise, we just animate between actual height (when open) and 0 (when closed).
height: open ? height ?? 'auto' : 0,
config: {
easing: open ? easings.easeInCubic : easings.easeOutCubic,
// Avoid animating if `open` is unchanged, so that nested AnimatedDropdowns don't stack and delay animations.
duration: open === wasOpen.current ? 0 : TRANSITION_DURATIONS.medium,
},
onStart: () => {
wasOpen.current = open
},
...springProps,
})
return (
<animated.div
style={{ ...props, overflow: 'hidden', width: '100%', minWidth: 'min-content', willChange: 'height' }}
>
<div ref={ref}>{children}</div>
</animated.div>
)
}
import { NumberValue, ScaleLinear, axisRight, Axis as d3Axis, select } from 'd3' import { NumberValue, ScaleLinear, axisLeft, Axis as d3Axis, select } from 'd3'
import styled from 'lib/styled-components' import styled from 'lib/styled-components'
import { useMemo } from 'react' import { useMemo } from 'react'
...@@ -16,8 +16,8 @@ const TEXT_Y_OFFSET = 10 ...@@ -16,8 +16,8 @@ const TEXT_Y_OFFSET = 10
const Axis = ({ const Axis = ({
axisGenerator, axisGenerator,
yScale,
height, height,
yScale,
}: { }: {
axisGenerator: d3Axis<NumberValue> axisGenerator: d3Axis<NumberValue>
height: number height: number
...@@ -32,12 +32,12 @@ const Axis = ({ ...@@ -32,12 +32,12 @@ const Axis = ({
g.selectAll('text').attr('transform', function (d) { g.selectAll('text').attr('transform', function (d) {
const yCoordinate = yScale(d as number) const yCoordinate = yScale(d as number)
if (yCoordinate < TEXT_Y_OFFSET) { if (yCoordinate < TEXT_Y_OFFSET) {
return `translate(0, ${TEXT_Y_OFFSET})` return `translate(0, ${TEXT_Y_OFFSET}) scale(-1,-1)`
} }
if (yCoordinate > height - TEXT_Y_OFFSET) { if (yCoordinate > height - TEXT_Y_OFFSET) {
return `translate(0, ${-TEXT_Y_OFFSET})` return `translate(0, ${-TEXT_Y_OFFSET}) scale(-1,-1)`
} }
return '' return 'scale(-1, -1)'
}), }),
) )
} }
...@@ -46,7 +46,7 @@ const Axis = ({ ...@@ -46,7 +46,7 @@ const Axis = ({
return <g ref={axisRef} /> return <g ref={axisRef} />
} }
export const AxisRight = ({ export const AxisLeft = ({
yScale, yScale,
offset = 0, offset = 0,
min, min,
...@@ -76,7 +76,7 @@ export const AxisRight = ({ ...@@ -76,7 +76,7 @@ export const AxisRight = ({
return ( return (
<StyledGroup transform={`translate(${offset}, 0)`}> <StyledGroup transform={`translate(${offset}, 0)`}>
<Axis axisGenerator={axisRight(yScale).tickValues(tickValues)} height={height} yScale={yScale} /> <Axis axisGenerator={axisLeft(yScale).tickValues(tickValues)} height={height} yScale={yScale} />
</StyledGroup> </StyledGroup>
) )
} }
...@@ -18,7 +18,6 @@ export const HorizontalArea = ({ ...@@ -18,7 +18,6 @@ export const HorizontalArea = ({
brushDomain, brushDomain,
selectedFill, selectedFill,
containerHeight, containerHeight,
containerWidth,
}: { }: {
series: ChartEntry[] series: ChartEntry[]
xScale: ScaleLinear<number, number> xScale: ScaleLinear<number, number>
...@@ -27,7 +26,6 @@ export const HorizontalArea = ({ ...@@ -27,7 +26,6 @@ export const HorizontalArea = ({
yValue: (d: ChartEntry) => number yValue: (d: ChartEntry) => number
brushDomain?: [number, number] brushDomain?: [number, number]
containerHeight: number containerHeight: number
containerWidth: number
fill?: string fill?: string
selectedFill?: string selectedFill?: string
}) => { }) => {
...@@ -44,9 +42,9 @@ export const HorizontalArea = ({ ...@@ -44,9 +42,9 @@ export const HorizontalArea = ({
return ( return (
<Bar <Bar
key={i} key={i}
x={xScale(xValue(d))} x={xScale(0)}
y={yScale(price)} y={yScale(price)}
width={xScale(containerWidth) - xScale(xValue(d))} width={xScale(xValue(d)) - xScale(0)}
height={0.2} height={0.2}
fill={isInDomain ? selectedFill : fill} fill={isInDomain ? selectedFill : fill}
rx={1} rx={1}
......
...@@ -181,7 +181,7 @@ async function calculateActiveRangeTokensLocked( ...@@ -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 */ /** Returns amounts of tokens locked in the given tick. Reference: https://docs.uniswap.org/sdk/v3/guides/advanced/active-liquidity */
export async function calculateTokensLocked( async function calculateTokensLocked(
token0: Token, token0: Token,
token1: Token, token1: Token,
feeTier: FeeAmount, feeTier: FeeAmount,
......
...@@ -343,7 +343,7 @@ exports[`LimitPriceInputPanel should render correct subheader with inputCurrency ...@@ -343,7 +343,7 @@ exports[`LimitPriceInputPanel should render correct subheader with inputCurrency
<h2 <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" 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" data-disable-theme="true"
id="title-:r3:" id="title-:r1:"
role="heading" role="heading"
/> />
</span> </span>
...@@ -696,7 +696,7 @@ exports[`LimitPriceInputPanel should render the component with no currencies sel ...@@ -696,7 +696,7 @@ exports[`LimitPriceInputPanel should render the component with no currencies sel
<h2 <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" 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" data-disable-theme="true"
id="title-:r1:" id="title-:r0:"
role="heading" role="heading"
/> />
</span> </span>
......
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