ci(release): publish latest release

parent f9a2e2e2
IPFS hash of the deployment:
- CIDv0: `QmNy6ppw64jmLBEZ6r8D19beUVH3objJPrjMfNxvugqakD`
- CIDv1: `bafybeiajkzyd25iwsu5lax4wtilh5ukji3kmzrt7r76k45pcminbsirsty`
- CIDv0: `Qma8zLjgStpH8bA8CcnrsPszS38XG4D9YqdW39VgnUSF64`
- CIDv1: `bafybeifpj54iwonrxyywl5g5crbd7krugzmgsr4lw6txrkygunexey66su`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
......@@ -10,14 +10,71 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeiajkzyd25iwsu5lax4wtilh5ukji3kmzrt7r76k45pcminbsirsty.ipfs.dweb.link/
- [ipfs://QmNy6ppw64jmLBEZ6r8D19beUVH3objJPrjMfNxvugqakD/](ipfs://QmNy6ppw64jmLBEZ6r8D19beUVH3objJPrjMfNxvugqakD/)
- https://bafybeifpj54iwonrxyywl5g5crbd7krugzmgsr4lw6txrkygunexey66su.ipfs.dweb.link/
- [ipfs://Qma8zLjgStpH8bA8CcnrsPszS38XG4D9YqdW39VgnUSF64/](ipfs://Qma8zLjgStpH8bA8CcnrsPszS38XG4D9YqdW39VgnUSF64/)
### 5.68.4 (2025-02-05)
## 5.69.0 (2025-02-05)
### Features
* **web:** add decimals to LP analytics properties to help debug (#15603) 9021bff
* **web:** add tests for getV3PriceRangeInfo (#15710) 2b941d0
* **web:** design update to buy form currency selector (#15370) ca2e42b
* **web:** hide/ unhide functionality for positions (#15158) ff6c74a
* **web:** hook visibility logic and ui into mini portfolio positions (#15368) 3283b9b
* **web:** prevent users from creating a pool with the wrapped native token (#15614) a36f3ac
* **web:** remove client side router code (#15423) d0bcd67
* **web:** show add button on PosDP for closed v3 positions (#15630) 0ab7ee2
### Bug Fixes
* **web:** use price to create mock pair (#15923) 40968c3
* **web:** allow creating v3 positions on celo/zksync (#15545) 35db486
* **web:** allow explore pool creation if disconnected (#15807) bd8ebe9
* **web:** Check hash of error in transaction before throwing Swap Failed error (#15767) 67b26f8
* **web:** do not normalize Amount, AmountChange, Dimensions, TimestampedAmount in apollo cache (#15579) 539fea5
* **web:** fix console error on token explore (#15573) ecd65b8
* **web:** fix dropdown options for LP position card (#15714) 6b92961
* **web:** fix initial setting in swap settings (#15302) f15c3d1
* **web:** fix issue with mnually inverted price on the confirm screen (#15776) 0c3e84b
* **web:** fix logging for explore table network filter (#15518) cc7a68e
* **web:** fix popover on position card (#15783) e51382a
* **web:** fix v2 migration endless nav loop (#15439) baba820
* **web:** handle text overflow on LP fee tier (#15537) 6fde371
* **web:** handle tick value of 0 (#15546) 69c3d69
* **web:** hide unichain promo tooltip on web positions page (#15552) 1b4d756
* **web:** include correct hook filter on ListPools queries (#15666) 2875b3c
* **web:** limit orders do not work with uniswapx v2 (again) (#15657) d9bcdcc
* **web:** lower data threshold for price range input charts (#15606) 052fda6
* **web:** lp chart range input auto suggestion bug staging (#15817) 8866017
* **web:** move migrate flow behind a feature flag (#15576) 572fee1
* **web:** only show scrollbars on NavDropdown when needed (#15617) 1f15564
* **web:** price range input fixes (#15739) 43620d4
* **web:** quick vs standard poll for backend orders (#15565) 4769ac3
* **web:** restrict fee tier chain filter to v3 (#15901) 21c32e7
* **web:** should redirect migrate page if account owner is not position owner (#15680) 7ad6efa
* **web:** skip failing gas fee query in LP create flow (#15792) 3eef945
* **web:** switch to new google customer account (#15913) b885080
* **web:** unichain modal content style adjustments (#15643) 4b33bd4
* **web:** update copy on native wrapped token (#15627) a548f5e
* **web:** update range overflow (#15564) 627d23d
* **web:** update send hook logic for ens lookup (#15531) dc9613d
* **web:** update the insufficient balance check to account for gas (#15797) b34461c
* **web:** use price to create mock pair (#15922) 122ac66
* **web:** use qn rpc for default and remove cloudflare eth (#15668) ff22d20
* **web:** user decimal seperator issue (#15574) 9808a42
* **web:** v4 PDP - use correct pool ID for price/volume queries (#15604) 6243d04
* **web:** zIndex issue with TDP share dropdown (#15884) cd3aa85
### Continuous Integration
* **web:** update sitemaps d038812
### Styles
* **web:** align modal close icons (#15507) 86111e1
web/5.68.4
\ No newline at end of file
web/5.69.0
\ No newline at end of file
......@@ -63,7 +63,7 @@ function PopupContent(): JSX.Element {
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius={6}
borderWidth={1}
borderWidth="$spacing1"
bottom={-spacing.spacing4}
p="$spacing2"
position="absolute"
......
import '@tamagui/core/reset.css'
import 'src/app/Global.css'
import { SharedEventName } from '@uniswap/analytics-events'
import { useEffect, useRef, useState } from 'react'
import { I18nextProvider } from 'react-i18next'
import { useDispatch } from 'react-redux'
......@@ -236,10 +237,11 @@ export default function SidebarApp(): JSX.Element {
}, [])
const isLoggedIn = useIsWalletUnlocked()
const hasSentLoginEvent = useRef(false)
const hasSentAppLoadEvent = useRef(false)
useEffect(() => {
if (isLoggedIn !== null && !hasSentLoginEvent.current) {
hasSentLoginEvent.current = true
if (isLoggedIn !== null && !hasSentAppLoadEvent.current) {
hasSentAppLoadEvent.current = true
sendAnalyticsEvent(SharedEventName.APP_LOADED)
sendAnalyticsEvent(ExtensionEventName.SidebarLoad, { locked: !isLoggedIn })
}
}, [isLoggedIn])
......
......@@ -21,7 +21,7 @@ export const Input = forwardRef<Input, InputProps>(function _Input(
backgroundColor={large ? '$surface1' : '$surface2'}
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
focusStyle={inputStyles.inputFocus}
fontSize={fonts.subheading2.fontSize}
height="auto"
......
......@@ -51,7 +51,7 @@ export const MnemonicViewer = ({ mnemonic }: { mnemonic?: string[] }): JSX.Eleme
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
gap="$spacing12"
position="relative"
px={px}
......
......@@ -19,7 +19,7 @@ function WalletSkeleton({ opacity }: { opacity: number }): JSX.Element {
alignItems="center"
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
height={WALLET_PREVIEW_CARD_HEIGHT}
justifyContent="flex-start"
opacity={opacity}
......
......@@ -84,7 +84,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-1 _alignItems-center _flexDirection-column _gap-t-space-spa94665593"
>
<div
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _backgroundColor-transparent _borderTopColor-transparent _borderRightColor-transparent _borderBottomColor-transparent _borderLeftColor-transparent _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-0px _borderRightWidth-0px _borderBottomWidth-0px _borderLeftWidth-0px _position-relative _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid"
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _backgroundColor-transparent _borderTopColor-transparent _borderRightColor-transparent _borderBottomColor-transparent _borderLeftColor-transparent _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-t-space-non101 _borderRightWidth-t-space-non101 _borderBottomWidth-t-space-non101 _borderLeftWidth-t-space-non101 _position-relative _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid"
data-testid="account-icon"
>
<svg
......@@ -205,7 +205,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _alignItems-center _cursor-pointer _gap-t-space-spa1360334080 _mt-t-space-spa1360334080 _pb-t-space-spa94665589 _pr-t-space-spa1360334080 _pl-t-space-spa1360334080"
>
<div
class="_display-flex _alignItems-center _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _justifyContent-center _backgroundColor-surface1 _borderTopColor-surface3 _borderRightColor-surface3 _borderBottomColor-surface3 _borderLeftColor-surface3 _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-1px _borderRightWidth-1px _borderBottomWidth-1px _borderLeftWidth-1px _height-40px _pt-t-space-spa94665593 _pr-t-space-spa94665593 _pb-t-space-spa94665593 _pl-t-space-spa94665593 _width-40px _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid _boxShadow-0px0px10pxv169431092"
class="_display-flex _alignItems-center _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _justifyContent-center _backgroundColor-surface1 _borderTopColor-surface3 _borderRightColor-surface3 _borderBottomColor-surface3 _borderLeftColor-surface3 _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-t-space-spa94665586 _borderRightWidth-t-space-spa94665586 _borderBottomWidth-t-space-spa94665586 _borderLeftWidth-t-space-spa94665586 _height-40px _pt-t-space-spa94665593 _pr-t-space-spa94665593 _pb-t-space-spa94665593 _pl-t-space-spa94665593 _width-40px _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid _boxShadow-0px0px10pxv169431092"
>
<svg
fill="none"
......@@ -316,7 +316,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-1 _alignItems-center _flexDirection-column _gap-t-space-spa94665593"
>
<div
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _backgroundColor-transparent _borderTopColor-transparent _borderRightColor-transparent _borderBottomColor-transparent _borderLeftColor-transparent _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-0px _borderRightWidth-0px _borderBottomWidth-0px _borderLeftWidth-0px _position-relative _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid"
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _backgroundColor-transparent _borderTopColor-transparent _borderRightColor-transparent _borderBottomColor-transparent _borderLeftColor-transparent _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-t-space-non101 _borderRightWidth-t-space-non101 _borderBottomWidth-t-space-non101 _borderLeftWidth-t-space-non101 _position-relative _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid"
data-testid="account-icon"
>
<svg
......@@ -437,7 +437,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _alignItems-center _cursor-pointer _gap-t-space-spa1360334080 _mt-t-space-spa1360334080 _pb-t-space-spa94665589 _pr-t-space-spa1360334080 _pl-t-space-spa1360334080"
>
<div
class="_display-flex _alignItems-center _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _justifyContent-center _backgroundColor-surface1 _borderTopColor-surface3 _borderRightColor-surface3 _borderBottomColor-surface3 _borderLeftColor-surface3 _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-1px _borderRightWidth-1px _borderBottomWidth-1px _borderLeftWidth-1px _height-40px _pt-t-space-spa94665593 _pr-t-space-spa94665593 _pb-t-space-spa94665593 _pl-t-space-spa94665593 _width-40px _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid _boxShadow-0px0px10pxv169431092"
class="_display-flex _alignItems-center _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _justifyContent-center _backgroundColor-surface1 _borderTopColor-surface3 _borderRightColor-surface3 _borderBottomColor-surface3 _borderLeftColor-surface3 _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-t-space-spa94665586 _borderRightWidth-t-space-spa94665586 _borderBottomWidth-t-space-spa94665586 _borderLeftWidth-t-space-spa94665586 _height-40px _pt-t-space-spa94665593 _pr-t-space-spa94665593 _pb-t-space-spa94665593 _pl-t-space-spa94665593 _width-40px _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid _boxShadow-0px0px10pxv169431092"
>
<svg
fill="none"
......
import { useSortedAccountList } from 'src/app/features/accounts/useSortedAccountList'
import { act, renderHook } from 'src/test/test-utils'
import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { useAccountListData } from 'wallet/src/features/accounts/useAccountListData'
jest.mock('wallet/src/features/accounts/hooks')
const mockUseAccountList = useAccountList as jest.MockedFunction<typeof useAccountList>
jest.mock('wallet/src/features/accounts/useAccountListData')
const mockUseAccountList = useAccountListData as jest.MockedFunction<typeof useAccountListData>
describe('useSortedAccountList', () => {
beforeEach(() => {
......
import { useMemo } from 'react'
import { usePrevious } from 'utilities/src/react/hooks'
import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { useAccountListData } from 'wallet/src/features/accounts/useAccountListData'
interface AddressWithBalance {
address: Address
......@@ -8,7 +8,7 @@ interface AddressWithBalance {
}
export function useSortedAccountList(addresses: Address[]): AddressWithBalance[] {
const { data: accountBalanceData } = useAccountList({
const { data: accountBalanceData } = useAccountListData({
addresses,
})
......
......@@ -45,6 +45,7 @@ interface DappRequestFooterProps {
showNetworkCost?: boolean
transactionGasFeeResult?: GasFeeResult
isUniswapX?: boolean
disableConfirm?: boolean
}
type DappRequestContentProps = DappRequestHeaderProps & DappRequestFooterProps
......@@ -88,6 +89,7 @@ export function DappRequestContent({
transactionGasFeeResult,
children,
isUniswapX,
disableConfirm,
}: PropsWithChildren<DappRequestContentProps>): JSX.Element {
const { forwards, currentIndex } = useDappRequestQueueContext()
......@@ -108,6 +110,7 @@ export function DappRequestContent({
showAllNetworks={showAllNetworks}
showNetworkCost={showNetworkCost}
transactionGasFeeResult={transactionGasFeeResult}
disableConfirm={disableConfirm}
onCancel={onCancel}
onConfirm={onConfirm}
/>
......@@ -162,6 +165,7 @@ export function DappRequestFooter({
showNetworkCost,
transactionGasFeeResult,
isUniswapX,
disableConfirm,
}: DappRequestFooterProps): JSX.Element {
const { t } = useTranslation()
const activeAccount = useActiveAccountWithThrow()
......@@ -253,7 +257,7 @@ export function DappRequestFooter({
{t('common.button.cancel')}
</DeprecatedButton>
<DeprecatedButton
disabled={!isConfirmEnabled}
disabled={!isConfirmEnabled || disableConfirm}
flex={1}
flexBasis={1}
size="medium"
......
......@@ -85,7 +85,10 @@ function* dappRequestApproval({
if (!senderTabId) {
throw new Error('senderTabId is required')
}
if (!requestId) {
if (requestId === false) {
// Check explicitly for false, since empty requestId string is also falsy.
// In the latter case, we need to proceed to remove the request from queue.
throw new Error('requestId is required')
}
......
......@@ -70,7 +70,7 @@ export function FallbackEthSendRequestContent({
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
gap="$spacing12"
p="$spacing16"
width="100%"
......@@ -100,7 +100,7 @@ export function FallbackEthSendRequestContent({
<Text
borderColor="$surface3"
borderRadius="$rounded8"
borderWidth={1}
borderWidth="$spacing1"
color="$neutral1"
// fontFamily="SF Mono"
px="$spacing8"
......
......@@ -33,7 +33,7 @@ export function LPRequestContent({
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
flexDirection="row"
justifyContent="space-between"
p="$spacing16"
......
......@@ -66,7 +66,7 @@ export function PersonalSignRequestContent({ dappRequest }: PersonalSignRequestP
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
flexDirection="row"
justifyContent="space-between"
maxHeight={200}
......
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent'
import { SignTypedDataRequest } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { Flex, Separator, Text } from 'ui/src'
import { Clear, Signature } from 'ui/src/components/icons'
import { InlineWarningCard } from 'uniswap/src/components/InlineWarningCard/InlineWarningCard'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
interface NonStandardTypedDataRequestContentProps {
dappRequest: SignTypedDataRequest
}
export function NonStandardTypedDataRequestContent({
dappRequest,
}: NonStandardTypedDataRequestContentProps): JSX.Element {
const { t } = useTranslation()
const [checked, setChecked] = useState(false)
const hasMessageToShow = !!dappRequest.typedData
return (
<DappRequestContent
showNetworkCost
confirmText={t('common.button.sign')}
title={t('dapp.request.signature.header')}
disableConfirm={!checked}
>
<Flex gap="$spacing16">
<Flex
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth="$spacing1"
flexDirection="column"
gap="$spacing12"
pt="$spacing12"
pb={!hasMessageToShow ? '$spacing12' : undefined}
overflow="hidden"
>
<Flex row px="$spacing12" gap="$spacing8" alignItems="center">
<Clear color="$neutral2" size="$icon.16" />
<Text variant="body3" color="$neutral2">
{t('dapp.request.signature.decodeError')}
</Text>
</Flex>
{hasMessageToShow && <Separator />}
{hasMessageToShow && (
<Flex maxHeight={150} $platform-web={{ overflowY: 'auto' }} px="$spacing16" gap="$spacing8">
<Flex row gap="$spacing8" alignItems="center">
<Signature color="$neutral2" size="$icon.16" />
<Text variant="body3" color="$neutral2">
{t('common.message')}
</Text>
</Flex>
<Text variant="body3" color="$neutral1">
{dappRequest.typedData}
</Text>
</Flex>
)}
</Flex>
<InlineWarningCard
hideCtaIcon
severity={WarningSeverity.Medium}
heading={t('dapp.request.signature.irregular')}
description={t('dapp.request.signature.irregular.description')}
checkboxLabel={t('dapp.request.signature.irregular.understand')}
checked={checked}
setChecked={setChecked}
/>
</Flex>
</DappRequestContent>
)
}
......@@ -34,7 +34,7 @@ export function Permit2RequestContent({ dappRequest }: Permit2RequestProps): JSX
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
flexDirection="column"
gap="$spacing12"
p="$spacing16"
......
import { Component, ErrorInfo, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent'
import { UniswapXSwapRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent'
import { DomainContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/DomainContent'
import { MaybeExplorerLinkedAddress } from 'src/app/features/dappRequests/requestContent/SignTypeData/MaybeExplorerLinkedAddress'
import { NonStandardTypedDataRequestContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent'
import { Permit2RequestContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent'
import { SignTypedDataRequest } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { EIP712Message, isEIP712TypedData } from 'src/app/features/dappRequests/types/EIP712Types'
......@@ -11,42 +13,69 @@ import { Flex, Text } from 'ui/src'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { isAddress } from 'utilities/src/addresses'
import { logger } from 'utilities/src/logger/logger'
interface SignTypedDataRequestProps {
dappRequest: SignTypedDataRequest
}
interface ErrorFallbackProps {
dappRequest: SignTypedDataRequest
}
function ErrorFallback({ dappRequest }: ErrorFallbackProps): JSX.Element {
return <NonStandardTypedDataRequestContent dappRequest={dappRequest} />
}
class SignTypedDataErrorBoundary extends Component<
{ children: ReactNode; dappRequest: SignTypedDataRequest },
{ hasError: boolean }
> {
constructor(props: { children: ReactNode; dappRequest: SignTypedDataRequest }) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
const { dappRequest } = this.props
logger.error(error, {
tags: { file: 'SignTypedDataRequestContent', function: 'ErrorBoundary' },
extra: {
errorInfo: JSON.stringify(errorInfo),
typedData: dappRequest.typedData,
address: dappRequest.address,
},
})
}
render(): ReactNode {
if (this.state.hasError) {
return <ErrorFallback dappRequest={this.props.dappRequest} />
}
return this.props.children
}
}
export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataRequestProps): JSX.Element | null {
return (
<SignTypedDataErrorBoundary dappRequest={dappRequest}>
<SignTypedDataRequestContentInner dappRequest={dappRequest} />
</SignTypedDataErrorBoundary>
)
}
function SignTypedDataRequestContentInner({ dappRequest }: SignTypedDataRequestProps): JSX.Element | null {
const { t } = useTranslation()
const parsedTypedData = JSON.parse(dappRequest.typedData)
if (!isEIP712TypedData(parsedTypedData)) {
return (
<DappRequestContent
showNetworkCost
confirmText={t('common.button.sign')}
title={t('dapp.request.signature.header')}
>
<Flex gap="$spacing12" p="$spacing16">
<Text>{t('dapp.request.signature.error.712-spec-compliance')}</Text>
<Flex
$platform-web={{ overflowY: 'auto' }}
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
flexDirection="column"
gap="$spacing4"
maxHeight={200}
p="$spacing12"
position="relative"
>
{dappRequest.typedData}
</Flex>
</Flex>
</DappRequestContent>
)
return <NonStandardTypedDataRequestContent dappRequest={dappRequest} />
}
const { name, version, chainId: domainChainId, verifyingContract, salt } = parsedTypedData?.domain || {}
......@@ -116,7 +145,7 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
flexDirection="column"
gap="$spacing4"
maxHeight={200}
......
......@@ -193,7 +193,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf
animation="quicker"
borderColor="$surface2"
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
disableRemoveScroll={false}
zIndex="$default"
{...animationPresets.fadeInDownOutUp}
......@@ -259,7 +259,7 @@ function ConnectionStatusIcon({
<Circle
backgroundColor="$statusSuccess"
borderColor="$surface1"
borderWidth={2}
borderWidth="$spacing2"
height={iconSizes.icon12}
mr="$spacing8"
position="absolute"
......
......@@ -72,7 +72,7 @@ export function SwitchNetworksModal(): JSX.Element {
<Separator mb="$spacing4" mt="$spacing8" />
<Flex shrink overflow="scroll">
<Flex shrink $platform-web={{ overflow: 'auto' }}>
{enabledChains.map((chain: UniverseChainId) => {
return (
<Popover.Close asChild>
......
......@@ -24,7 +24,7 @@ export function PinReminder({
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
gap="$spacing12"
p="$spacing12"
shadowColor="$shadowColor"
......
......@@ -198,7 +198,7 @@ export function ImportMnemonic(): JSX.Element {
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius={100}
borderWidth={1}
borderWidth="$spacing1"
mt="$spacing8"
px="$spacing12"
py="$spacing8"
......@@ -307,7 +307,7 @@ const RecoveryPhraseWord = forwardRef<
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
focusStyle={styles.inputFocus}
fontSize={fonts.body3.fontSize}
height={44}
......
......@@ -8,7 +8,7 @@ export function MainContentWrapper({ children }: PropsWithChildren): JSX.Element
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded32"
borderWidth={1}
borderWidth="$spacing1"
pb="$spacing24"
pt="$spacing48"
px="$spacing24"
......
......@@ -17,7 +17,7 @@ export function MainIntroWrapper({
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded32"
borderWidth={1}
borderWidth="$spacing1"
overflow="hidden"
shadowColor="$shadowColor"
shadowOpacity={0.1}
......
......@@ -197,7 +197,7 @@ export function OTPInput(): JSX.Element {
backgroundColor={character ? '$surface1' : '$surface2'}
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
disabled={loading}
focusStyle={inputStyles.inputFocus}
fontSize={fonts.heading3.fontSize}
......
......@@ -14,12 +14,12 @@ import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingSteps'
import { useScantasticContext } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
import { getScantasticUrl } from 'src/app/features/onboarding/scan/utils'
import { TopLevelRoutes } from 'src/app/navigation/constants'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import UAParser from 'ua-parser-js'
import { Flex, Image, Square, Text, useSporeColors } from 'ui/src'
import { Flex, Image, Square, Text, TouchableArea, useSporeColors } from 'ui/src'
import { DOT_GRID, UNISWAP_LOGO } from 'ui/src/assets'
import { Mobile, Wifi } from 'ui/src/components/icons'
import { FileListLock, Mobile, RotatableChevron, Wifi } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes, zIndices } from 'ui/src/theme'
import { uniswapUrls } from 'uniswap/src/constants/urls'
......@@ -82,7 +82,6 @@ export function ScanToOnboard(): JSX.Element {
browser,
model,
})
return getScantasticUrl(params)
} catch (e) {
const wrappedError = new Error('Failed to build scantastic params', { cause: e })
......@@ -189,6 +188,53 @@ export function ScanToOnboard(): JSX.Element {
screen={ExtensionOnboardingScreens.OnboardingQRCode}
>
<OnboardingScreen
belowFrameContent={
errorDerivingQR ? (
<Flex centered width="100%">
<TouchableArea
borderRadius="$rounded20"
zIndex={zIndices.fixed}
onPress={(): void => navigate(`/${TopLevelRoutes.Onboarding}/${OnboardingRoutes.Import}`)}
>
<Flex
alignContent="center"
alignItems="center"
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$roundedFull"
borderWidth="$spacing1"
my="$spacing12"
shadowColor="$shadowColor"
shadowOpacity={0.4}
shadowRadius="$spacing4"
pr="$spacing16"
pl="$spacing20"
py="$spacing12"
>
<Flex row centered gap="$spacing8" justifyContent="space-between">
<FileListLock color="$accent1" size="$icon.36" />
<Flex shrink flexWrap="wrap">
<Text color="$neutral2" variant="body3">
{t('onboarding.scan.troubleScanning.title')}
</Text>
<Text color="$accent1" variant="buttonLabel2">
{t('onboarding.scan.troubleScanning.message')}
</Text>
</Flex>
<RotatableChevron
color="$neutral3"
direction="end"
height={iconSizes.icon24}
width={iconSizes.icon24}
/>
</Flex>
</Flex>
</TouchableArea>
</Flex>
) : undefined
}
Icon={
<Square
backgroundColor="$surface2"
......@@ -210,11 +256,10 @@ export function ScanToOnboard(): JSX.Element {
<Flex
alignContent="center"
alignItems="center"
backgroundColor={colors.white.val}
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth="$spacing1"
flexDirection="column"
my="$spacing24"
p="$spacing16"
position="relative"
......@@ -223,8 +268,8 @@ export function ScanToOnboard(): JSX.Element {
shadowRadius="$spacing4"
>
{errorDerivingQR ? (
<Flex height={QR_CODE_SIZE} width={QR_CODE_SIZE}>
<Text color="$statusCritical" m="auto" textAlign="center" variant="body2">
<Flex px="$spacing16" height={QR_CODE_SIZE} width={QR_CODE_SIZE}>
<Text color="$neutral2" m="auto" textAlign="center" variant="body3">
{t('onboarding.scan.error')}
</Text>
</Flex>
......
......@@ -5961,7 +5961,7 @@ exports[`ReceiveScreen renders without error 1`] = `
class="_display-flex _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _alignItems-center _backgroundColor-transparent _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _overflowX-visible _overflowY-visible _pl-t-space-spa94665587 _position-absolute _pt-t-space-spa94665587"
>
<div
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _backgroundColor-surface1 _borderTopColor-surface1 _borderRightColor-surface1 _borderBottomColor-surface1 _borderLeftColor-surface1 _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-4px _borderRightWidth-4px _borderBottomWidth-4px _borderLeftWidth-4px _position-relative _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid"
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _backgroundColor-surface1 _borderTopColor-surface1 _borderRightColor-surface1 _borderBottomColor-surface1 _borderLeftColor-surface1 _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-t-space-spa94665589 _borderRightWidth-t-space-spa94665589 _borderBottomWidth-t-space-spa94665589 _borderLeftWidth-t-space-spa94665589 _position-relative _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid"
data-testid="account-icon"
>
<svg
......@@ -11997,7 +11997,7 @@ exports[`ReceiveScreen renders without error 1`] = `
class="_display-flex _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _alignItems-center _backgroundColor-transparent _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _overflowX-visible _overflowY-visible _pl-t-space-spa94665587 _position-absolute _pt-t-space-spa94665587"
>
<div
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _backgroundColor-surface1 _borderTopColor-surface1 _borderRightColor-surface1 _borderBottomColor-surface1 _borderLeftColor-surface1 _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-4px _borderRightWidth-4px _borderBottomWidth-4px _borderLeftWidth-4px _position-relative _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid"
class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _backgroundColor-surface1 _borderTopColor-surface1 _borderRightColor-surface1 _borderBottomColor-surface1 _borderLeftColor-surface1 _borderTopLeftRadius-t-radius-ro1041013639 _borderTopRightRadius-t-radius-ro1041013639 _borderBottomRightRadius-t-radius-ro1041013639 _borderBottomLeftRadius-t-radius-ro1041013639 _borderTopWidth-t-space-spa94665589 _borderRightWidth-t-space-spa94665589 _borderBottomWidth-t-space-spa94665589 _borderLeftWidth-t-space-spa94665589 _position-relative _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid"
data-testid="account-icon"
>
<svg
......
......@@ -10,7 +10,7 @@ import { iconSizes } from 'ui/src/theme'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { useAccountListData } from 'wallet/src/features/accounts/useAccountListData'
import { Account } from 'wallet/src/features/wallet/accounts/types'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
......@@ -45,7 +45,7 @@ export function RemoveRecoveryPhraseWallets(): JSX.Element {
// TODO(@thomasthachil): merge this with mobile AccountList
function AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element {
const addresses = useMemo(() => accounts.map((account) => account.address), [accounts])
const { data, loading } = useAccountList({
const { data, loading } = useAccountListData({
addresses,
notifyOnNetworkStatusChange: true,
})
......@@ -58,7 +58,7 @@ function AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Elem
.sort((a, b) => (b.balance ?? 0) - (a.balance ?? 0))
return (
<Flex borderColor="$surface3" borderRadius="$rounded20" borderWidth={1} px="$spacing12" width="100%">
<Flex borderColor="$surface3" borderRadius="$rounded20" borderWidth="$spacing1" px="$spacing12" width="100%">
<ScrollView bounces={false}>
{sortedAddressesByBalance.map(({ address, balance }, index) => (
<AssociatedAccountRow
......
......@@ -115,7 +115,7 @@ export function SeedPhraseDisplay({ mnemonicId }: { mnemonicId: string }): JSX.E
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
gap="$spacing12"
width="100%"
>
......
......@@ -61,7 +61,7 @@ export function UnitagIntroScreen(): JSX.Element {
function UnitagIntroPill({ Icon, text }: { Icon: GeneratedIcon; text: string }): JSX.Element {
return (
<Flex row gap="$spacing8" p="$spacing12" borderWidth={1} borderColor="$surface3" borderRadius="$rounded16">
<Flex row gap="$spacing8" p="$spacing12" borderWidth="$spacing1" borderColor="$surface3" borderRadius="$rounded16">
<Icon color="$accent1" size="$icon.24" />
<Text color="$neutral2" variant="body1">
{text}
......
......@@ -19,7 +19,7 @@ export const BaseEthereumRequestSchema = z.object({
})
export const EthereumRequestWithIdSchema = BaseEthereumRequestSchema.extend({
requestId: z.string(),
requestId: z.string().uuid(),
})
export type EthereumRequestWithId = z.infer<typeof EthereumRequestWithIdSchema>
......
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.15.0",
"version": "1.16.0",
"minimum_chrome_version": "116",
"icons": {
"16": "assets/icon16.png",
......
......@@ -13,6 +13,7 @@ import {
v15Schema,
v16Schema,
v17Schema,
v18Schema,
v1Schema,
v2Schema,
v3Schema,
......@@ -43,6 +44,7 @@ import {
testActivatePendingAccounts,
testAddCreatedOnboardingRedesignAccount,
testAddedHapticSetting,
testDeleteWelcomeWalletCard,
testMovedCurrencySetting,
testMovedLanguageSetting,
testMovedTokenWarnings,
......@@ -280,4 +282,8 @@ describe('Redux state migrations', () => {
it('migrates from v17 to v18', () => {
testUnchecksumDismissedTokenWarningKeys(migrations[18], v17Schema)
})
it('migrates from v18 to v19', () => {
testDeleteWelcomeWalletCard(migrations[19], v18Schema)
})
})
......@@ -12,6 +12,7 @@ import {
deleteDefaultFavoritesFromFavoritesState,
deleteExtensionOnboardingState,
deleteHoldToSwapBehaviorHistory,
deleteWelcomeWalletCardBehaviorHistory,
moveCurrencySetting,
moveDismissedTokenWarnings,
moveLanguageSetting,
......@@ -44,6 +45,7 @@ export const migrations = {
16: updateExploreOrderByType,
17: removeCreatedOnboardingRedesignAccountBehaviorHistory,
18: unchecksumDismissedTokenWarningKeys,
19: deleteWelcomeWalletCardBehaviorHistory,
}
export const EXTENSION_STATE_VERSION = 18
export const EXTENSION_STATE_VERSION = 19
......@@ -204,4 +204,14 @@ const v17SchemaIntermediate = {
delete v17SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount
export const v17Schema = v17SchemaIntermediate
export const getSchema = (): typeof v17Schema => v17Schema
const v18SchemaIntermediate = {
...v17Schema,
behaviorHistory: {
...v17Schema.behaviorHistory,
hasViewedWelcomeWalletCard: undefined,
},
}
delete v18SchemaIntermediate.behaviorHistory.hasViewedWelcomeWalletCard
export const v18Schema = v18SchemaIntermediate
export const getSchema = (): typeof v18Schema => v18Schema
......@@ -114,6 +114,7 @@ ios/WidgetsCore/MobileSchema/*
# Swift env
ios/WidgetsCore/Env.swift
ios/OneSignalNotificationServiceExtension/Env.swift
# Sentry
ios/sentry.properties
......
......@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isE2E) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
}
def devVersionName = "1.45"
def betaVersionName = "1.45"
def prodVersionName = "1.45"
def devVersionName = "1.46"
def betaVersionName = "1.46"
def prodVersionName = "1.46"
android {
ndkVersion rootProject.ext.ndkVersion
......
......@@ -77,7 +77,7 @@ class NotificationExtension : OSRemoteNotificationReceivedHandler {
private const val STATSIG_ENVIRONMENT_KEY_TIER = "tier"
private const val FEATURE_GATE_UNFUNDED_WALLET = "notification_unfunded_wallet"
private const val FEATURE_GATE_PRICE_ALERT = "notification_price_alert"
private const val FEATURE_GATE_PRICE_ALERT = "notification_price_alerts"
private const val FIELD_NOTIFICATION_TYPE = "notification_type"
private const val TYPE_UNFUNDED_WALLET_REMINDER = "unfunded_wallet_reminder"
......
......@@ -11,8 +11,6 @@
</dict>
<key>OneSignal_app_groups_key</key>
<string>group.com.uniswap.mobile.onesignal</string>
<key>STATSIG_SDK_KEY</key>
<string>$(STATSIG_SDK_KEY)</string>
<key>BUNDLE_ID_SUFFIX</key>
<string>$(BUNDLE_ID_SUFFIX)</string>
</dict>
......
......@@ -43,7 +43,7 @@ class NotificationService: UNNotificationServiceExtension {
if (!Statsig.isInitialized()) {
// The real sdk key is needed on iOS even though it's substituted in proxy
// Because the key is used to hash the feature gate names and wouldn't work properly otherwise
let statsigSdkKey = Bundle.main.object(forInfoDictionaryKey: "STATSIG_SDK_KEY") as? String ?? ""
let statsigSdkKey = Env.STATSIG_API_KEY
let statsigUser = StatsigUser(
userID: UIDevice.current.identifierForVendor?.uuidString,
custom: [
......@@ -88,5 +88,5 @@ struct Constants {
static let typePriceAlert = "price_alert"
static let gateUnfundedWallet = "notification_unfunded_wallet"
static let gatePriceAlert = "notification_price_alert"
static let gatePriceAlert = "notification_price_alerts"
}
......@@ -1204,7 +1204,7 @@ PODS:
- ExpoModulesCore
- ExpoBlur (12.9.2):
- ExpoModulesCore
- ExpoCamera (14.1.2):
- ExpoCamera (14.1.1):
- ExpoModulesCore
- ZXingObjC/OneD
- ZXingObjC/PDF417
......@@ -3014,7 +3014,7 @@ SPEC CHECKSUMS:
EXImageLoader: 55080616b2fe9da19ef8c7f706afd9814e279b6b
Expo: 0f04015e4254c8b7c74f3c4b2facdfd0b62152b0
ExpoBlur: e832d874bd94afc0645daddbd3162ec1ce172080
ExpoCamera: ed3dd4ae8e32603c91336ea8e27c5d1e2822d8f6
ExpoCamera: 53f5ef81bab088b2edcf94d05e4da3bc84f46383
ExpoClipboard: b597982124f067ff9f5b89093eb3d97898d5d877
ExpoFileSystem: eecaf6796aed0f4dd20042dc2ca2cac6c4bc1185
ExpoHaptics: 28a771b630353cd6e8dcf1b1e3e693e38ad7c3c3
......
......@@ -28,6 +28,7 @@ jest.mock('react-native-onesignal', () => {
promptForPushNotificationsWithUserResponse: jest.fn(),
setNotificationWillShowInForegroundHandler: jest.fn(),
setNotificationOpenedHandler: jest.fn(),
sendTag: jest.fn(),
getDeviceState: () => ({ userId: 'dummyUserId', pushToken: 'dummyPushToken' }),
}
})
......
......@@ -11,6 +11,7 @@
"android:prod": "react-native run-android --mode=prodDebug",
"android:prod:release": "react-native run-android --mode=prodRelease",
"check:deps:usage": "./scripts/checkDepsUsage.sh",
"check:bundlesize": "./scripts/checkBundleSize.sh",
"clean": "react-native-clean-project",
"debug": "react-devtools",
"debug:reactotron:install": "./scripts/installDebugger.sh",
......@@ -26,13 +27,13 @@
"firestore:deploy:rules": "firebase deploy --only firestore:rules",
"link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 2",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 1",
"ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios",
"ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift",
"ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"",
"ios:dev:release": "react-native run-ios --configuration Dev",
"ios:beta": "react-native run-ios --configuration Beta",
"ios:bundle": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios' --assets-dest='./ios'",
"ios:bundle": "react-native bundle --entry-file='index.js' --dev false --bundle-output='./ios/main.jsbundle' --sourcemap-output ./ios/main.jsbundle.map --dev=false --platform='ios' --assets-dest='./ios'",
"ios:release": "react-native run-ios --configuration Release",
"format": "../../scripts/prettier.sh",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0",
......@@ -99,7 +100,7 @@
"expo": "50.0.15",
"expo-barcode-scanner": "12.9.3",
"expo-blur": "12.9.2",
"expo-camera": "14.1.2",
"expo-camera": "14.1.1",
"expo-clipboard": "5.0.1",
"expo-haptics": "12.8.1",
"expo-linear-gradient": "12.7.2",
......
#!/bin/bash
MAX_SIZE=20.75
# Check OS type and use appropriate stat command
if [[ "$OSTYPE" == "darwin"* ]]; then
# MacOS
BUNDLE_SIZE=$(stat -f %z ios/main.jsbundle | awk '{print $1/1024/1024}')
else
# Linux and others
BUNDLE_SIZE=$(stat --format=%s ios/main.jsbundle | awk '{print $1/1024/1024}')
fi
if (( $(echo "$BUNDLE_SIZE > $MAX_SIZE" | bc -l) )); then
echo "Bundle size ($BUNDLE_SIZE MB) exceeds limit ($MAX_SIZE MB)"
exit 1
else
echo "✅ Bundle size ($BUNDLE_SIZE MB) is within limit ($MAX_SIZE MB)"
fi
......@@ -2,8 +2,8 @@ import os
ENV_DEFAULTS_FILE = '../../.env.defaults'
ENV_DEFAULTS_LOCAL_FILE = '../../.env.defaults.local'
SWIFT_FILE_PATH = 'ios/WidgetsCore/Env.swift'
SWIFT_ENV_VARIABLES = ['UNISWAP_API_KEY']
SWIFT_FILE_PATHS = ['ios/WidgetsCore/Env.swift', 'ios/OneSignalNotificationServiceExtension/Env.swift']
SWIFT_ENV_VARIABLES = ['UNISWAP_API_KEY', 'STATSIG_API_KEY']
def to_swift_constant_line(key, value):
return f' static let {key.upper()} = "{value}"'
......@@ -22,7 +22,7 @@ def process_lines(lines, search_vars):
return env_var_declarations
# convert env variables to swift constants and writes to a swift file.
def copy_env_vars_to_swift(env_defaults_file, env_defaults_local_file, swift_file, env_variables):
def copy_env_vars_to_swift(env_defaults_file, env_defaults_local_file, swift_files, env_variables):
envs_left_to_find = env_variables.copy()
env_var_declarations = []
......@@ -44,6 +44,7 @@ def copy_env_vars_to_swift(env_defaults_file, env_defaults_local_file, swift_fil
env_var_declarations.extend(process_lines(default_env_lines, envs_left_to_find))
# write to swift file
for swift_file in swift_files:
with open(swift_file, 'w') as f:
f.write('struct Env {\n')
f.write('\n'.join(env_var_declarations))
......@@ -54,4 +55,4 @@ def copy_env_vars_to_swift(env_defaults_file, env_defaults_local_file, swift_fil
print('WARNING: Not all environment variables were converted to Swift.')
exit(1)
copy_env_vars_to_swift(ENV_DEFAULTS_FILE, ENV_DEFAULTS_LOCAL_FILE, SWIFT_FILE_PATH, SWIFT_ENV_VARIABLES)
copy_env_vars_to_swift(ENV_DEFAULTS_FILE, ENV_DEFAULTS_LOCAL_FILE, SWIFT_FILE_PATHS, SWIFT_ENV_VARIABLES)
......@@ -11,6 +11,7 @@ import appsFlyer from 'react-native-appsflyer'
import DeviceInfo from 'react-native-device-info'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { MMKV } from 'react-native-mmkv'
import OneSignal from 'react-native-onesignal'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens'
import { useDispatch, useSelector } from 'react-redux'
......@@ -29,8 +30,9 @@ import { LockScreenContextProvider } from 'src/features/authentication/lockScree
import { BiometricContextProvider } from 'src/features/biometrics/context'
import { NotificationToastWrapper } from 'src/features/notifications/NotificationToastWrapper'
import { initOneSignal } from 'src/features/notifications/Onesignal'
import { AIAssistantScreen } from 'src/features/openai/AIAssistantScreen'
import { OpenAIContextProvider } from 'src/features/openai/OpenAIContext'
import { OneSignalUserTagField } from 'src/features/notifications/constants'
import { DevAIAssistantScreen } from 'src/features/openai/DevAIAssistantScreen'
import { DevOpenAIProvider } from 'src/features/openai/DevOpenAIProvider'
import { shouldLogScreen } from 'src/features/telemetry/directLogScreens'
import { selectCustomEndpoint } from 'src/features/tweaks/selectors'
import {
......@@ -55,7 +57,7 @@ import {
import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants'
import { Experiments } from 'uniswap/src/features/gating/experiments'
import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags'
import { getDynamicConfigValue, useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { getDynamicConfigValue, getFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { loadStatsigOverrides } from 'uniswap/src/features/gating/overrides/customPersistedOverrides'
import { Statsig, StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
......@@ -74,6 +76,7 @@ import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utiliti
import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { DDRumAction, DDRumTiming } from 'utilities/src/logger/datadogEvents'
import { logger } from 'utilities/src/logger/logger'
import { isIOS } from 'utilities/src/platform'
import { useAsyncData } from 'utilities/src/react/hooks'
import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
......@@ -250,6 +253,17 @@ function AppOuter(): JSX.Element | null {
Statsig.getExperimentWithExposureLoggingDisabled(experiment).getGroupName(),
).catch(() => undefined)
}
// Used in case we aren't able to resolve notification filtering issues on iOS
if (isIOS) {
const notificationsPriceAlertsEnabled = getFeatureFlag(FeatureFlags.NotificationPriceAlerts)
const notificationsUnfundedWalletEnabled = getFeatureFlag(FeatureFlags.NotificationUnfundedWallets)
OneSignal.sendTags({
[OneSignalUserTagField.GatingPriceAlertsEnabled]: notificationsPriceAlertsEnabled ? 'true' : 'false',
[OneSignalUserTagField.GatingUnfundedWalletsEnabled]: notificationsUnfundedWalletEnabled ? 'true' : 'false',
})
}
}, [])
if (!client) {
......@@ -270,7 +284,7 @@ function AppOuter(): JSX.Element | null {
<DataUpdaters />
<NavigationContainer>
<MobileWalletNavigationProvider>
<OpenAIContextProvider>
<DevOpenAIProvider>
<WalletUniswapProvider>
<BottomSheetModalProvider>
<AppModals />
......@@ -280,7 +294,7 @@ function AppOuter(): JSX.Element | null {
</BottomSheetModalProvider>
</WalletUniswapProvider>
<NotificationToastWrapper />
</OpenAIContextProvider>
</DevOpenAIProvider>
</MobileWalletNavigationProvider>
</NavigationContainer>
</LockScreenContextProvider>
......@@ -331,11 +345,9 @@ function AppInner(): JSX.Element {
NativeModules.ThemeModule.setColorScheme(themeSetting)
}, [themeSetting])
const openAIAssistantEnabled = useFeatureFlag(FeatureFlags.OpenAIAssistant)
return (
<>
{openAIAssistantEnabled && <AIAssistantScreen />}
<DevAIAssistantScreen />
<OfflineBanner />
<TestnetModeBanner />
<AppStackNavigator />
......
......@@ -84,6 +84,7 @@ import {
v7Schema,
v80Schema,
v81Schema,
v83Schema,
v8Schema,
v9Schema,
} from 'src/app/schema'
......@@ -92,6 +93,7 @@ import { initialBiometricsSettingsState } from 'src/features/biometrics/slice'
import { initialCloudBackupState } from 'src/features/CloudBackup/cloudBackupSlice'
import { initialPasswordLockoutState } from 'src/features/CloudBackup/passwordLockoutSlice'
import { initialModalsState } from 'src/features/modals/modalSlice'
import { initialPushNotificationsState } from 'src/features/notifications/slice'
import { initialTweaksState } from 'src/features/tweaks/slice'
import { initialWalletConnectState } from 'src/features/walletConnect/walletConnectSlice'
import { AccountType } from 'uniswap/src/features/accounts/types'
......@@ -120,6 +122,7 @@ import {
testActivatePendingAccounts,
testAddCreatedOnboardingRedesignAccount,
testAddedHapticSetting,
testDeleteWelcomeWalletCard,
testMovedCurrencySetting,
testMovedLanguageSetting,
testMovedTokenWarnings,
......@@ -194,6 +197,7 @@ describe('Redux state migrations', () => {
passwordLockout: initialPasswordLockoutState,
behaviorHistory: initialBehaviorHistoryState,
providers: { isInitialized: false },
pushNotifications: initialPushNotificationsState,
saga: {},
searchHistory: initialSearchHistoryState,
telemetry: initialTelemetryState,
......@@ -1598,4 +1602,17 @@ describe('Redux state migrations', () => {
it('migrates from v81 to v82', () => {
testUnchecksumDismissedTokenWarningKeys(migrations[82], v81Schema)
})
it('migrates from v82 to v83', () => {
// v82 didn't have a new schema
const v81Stub = { ...v81Schema }
const v83 = migrations[83](v81Stub)
expect(v83.pushNotifications.generalUpdatesEnabled).toBe(false)
expect(v83.pushNotifications.priceAlertsEnabled).toBe(false)
})
it('migrates from v83 to v84', () => {
testDeleteWelcomeWalletCard(migrations[84], v83Schema)
})
})
......@@ -30,6 +30,7 @@ import {
deleteDefaultFavoritesFromFavoritesState,
deleteExtensionOnboardingState,
deleteHoldToSwapBehaviorHistory,
deleteWelcomeWalletCardBehaviorHistory,
moveCurrencySetting,
moveDismissedTokenWarnings,
moveLanguageSetting,
......@@ -957,6 +958,27 @@ export const migrations = {
81: removeCreatedOnboardingRedesignAccountBehaviorHistory,
82: unchecksumDismissedTokenWarningKeys,
83: function addPushNotifications(state: any) {
// Enabling new notifications unless they have all wallet activity notifs disabled
const hasAllWalletNotifsDisabled = Object.values(state.wallet.accounts).every(
(account) =>
account &&
typeof account === 'object' &&
'pushNotificationsEnabled' in account &&
!account.pushNotificationsEnabled,
)
return {
...state,
pushNotifications: {
generalUpdatesEnabled: !hasAllWalletNotifsDisabled,
priceAlertsEnabled: !hasAllWalletNotifsDisabled,
},
}
},
84: deleteWelcomeWalletCardBehaviorHistory,
}
export const MOBILE_STATE_VERSION = 82
export const MOBILE_STATE_VERSION = 84
import { combineReducers } from '@reduxjs/toolkit'
import { monitoredSagaReducers } from 'src/app/saga'
import { monitoredSagaReducers } from 'src/app/monitoredSagas'
import { cloudBackupReducer } from 'src/features/CloudBackup/cloudBackupSlice'
import { passwordLockoutReducer } from 'src/features/CloudBackup/passwordLockoutSlice'
import { biometricSettingsReducer } from 'src/features/biometrics/slice'
import { modalsReducer } from 'src/features/modals/modalSlice'
import { pushNotificationsReducer } from 'src/features/notifications/slice'
import { tweaksReducer } from 'src/features/tweaks/slice'
import { walletConnectReducer } from 'src/features/walletConnect/walletConnectSlice'
import { walletPersistedStateList, walletReducers } from 'wallet/src/state/walletReducer'
......@@ -14,6 +15,7 @@ const mobileReducers = {
cloudBackup: cloudBackupReducer,
modals: modalsReducer,
passwordLockout: passwordLockoutReducer,
pushNotifications: pushNotificationsReducer,
saga: monitoredSagaReducers,
tweaks: tweaksReducer,
walletConnect: walletConnectReducer,
......@@ -27,6 +29,7 @@ export const mobilePersistedStateList: Array<keyof typeof mobileReducers> = [
'passwordLockout',
'tweaks',
'cloudBackup',
'pushNotifications',
]
export type MobileState = ReturnType<typeof mobileReducer>
......@@ -70,6 +70,7 @@ export function NotificationsOSSettingsModal({ navigation }: NotificationsOSSett
<Flex animation="fast" gap="$spacing40" pb="$spacing12" px="$spacing24" width="100%">
<GenericHeader
Icon={BellOn}
flexProps={{ m: '$spacing12' }}
subtitle={t('onboarding.notification.subtitle')}
title={t('onboarding.notification.title')}
/>
......
import { swapActions, swapReducer, swapSaga, swapSagaName } from 'wallet/src/features/transactions/swap/swapSaga'
import {
tokenWrapActions,
tokenWrapReducer,
tokenWrapSaga,
tokenWrapSagaName,
} from 'wallet/src/features/transactions/swap/wrapSaga'
import {
editAccountActions,
editAccountReducer,
editAccountSaga,
editAccountSagaName,
} from 'wallet/src/features/wallet/accounts/editAccountSaga'
import {
createAccountsActions,
createAccountsReducer,
createAccountsSaga,
createAccountsSagaName,
} from 'wallet/src/features/wallet/create/createAccountsSaga'
import { MonitoredSaga, getMonitoredSagaReducers } from 'wallet/src/state/saga'
// All monitored sagas must be included here
export const monitoredSagas: Record<string, MonitoredSaga> = {
[createAccountsSagaName]: {
name: createAccountsSagaName,
wrappedSaga: createAccountsSaga,
reducer: createAccountsReducer,
actions: createAccountsActions,
},
[editAccountSagaName]: {
name: editAccountSagaName,
wrappedSaga: editAccountSaga,
reducer: editAccountReducer,
actions: editAccountActions,
},
[swapSagaName]: {
name: swapSagaName,
wrappedSaga: swapSaga,
reducer: swapReducer,
actions: swapActions,
},
[tokenWrapSagaName]: {
name: tokenWrapSagaName,
wrappedSaga: tokenWrapSaga,
reducer: tokenWrapReducer,
actions: tokenWrapActions,
},
}
export const monitoredSagaReducers = getMonitoredSagaReducers(monitoredSagas)
......@@ -441,6 +441,9 @@ export function AppStackNavigator(): JSX.Element {
<AppStack.Group screenOptions={navNativeStackOptions.presentationModal}>
<AppStack.Screen component={EducationScreen} name={MobileScreens.Education} />
</AppStack.Group>
<AppStack.Group screenOptions={navNativeStackOptions.presentationBottomSheet}>
<AppStack.Screen component={NotificationsOSSettingsModal} name={ModalName.NotificationsOSSettings} />
</AppStack.Group>
{isDevEnv() && <AppStack.Screen component={StorybookUIRoot} name={MobileScreens.Storybook} />}
</AppStack.Navigator>
)
......
......@@ -124,6 +124,7 @@ export type AppStackParamList = {
}
[MobileScreens.WebView]: { headerTitle: string; uriLink: string }
[MobileScreens.Storybook]: undefined
[ModalName.NotificationsOSSettings]: undefined
}
export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList>
......
import { PersistState } from 'redux-persist'
import { monitoredSagas } from 'src/app/monitoredSagas'
import { cloudBackupsManagerSaga } from 'src/features/CloudBackup/saga'
import { appRatingWatcherSaga } from 'src/features/appRating/saga'
import { deepLinkWatcher } from 'src/features/deepLinking/handleDeepLinkSaga'
import { firebaseDataWatcher } from 'src/features/firebase/firebaseDataSaga'
import { modalWatcher } from 'src/features/modals/saga'
import { pushNotificationsWatcherSaga } from 'src/features/notifications/saga'
import { telemetrySaga } from 'src/features/telemetry/saga'
import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga'
import { walletConnectSaga } from 'src/features/walletConnect/saga'
......@@ -11,27 +13,7 @@ import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga'
import { call, delay, select, spawn } from 'typed-redux-saga'
import { appLanguageWatcherSaga } from 'uniswap/src/features/language/saga'
import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient'
import { swapActions, swapReducer, swapSaga, swapSagaName } from 'wallet/src/features/transactions/swap/swapSaga'
import {
tokenWrapActions,
tokenWrapReducer,
tokenWrapSaga,
tokenWrapSagaName,
} from 'wallet/src/features/transactions/swap/wrapSaga'
import { transactionWatcher } from 'wallet/src/features/transactions/transactionWatcherSaga'
import {
editAccountActions,
editAccountReducer,
editAccountSaga,
editAccountSagaName,
} from 'wallet/src/features/wallet/accounts/editAccountSaga'
import {
createAccountsActions,
createAccountsReducer,
createAccountsSaga,
createAccountsSagaName,
} from 'wallet/src/features/wallet/create/createAccountsSaga'
import { MonitoredSaga, getMonitoredSagaReducers } from 'wallet/src/state/saga'
const REHYDRATION_STATUS_POLLING_INTERVAL = 50
......@@ -43,42 +25,13 @@ const sagas = [
deepLinkWatcher,
firebaseDataWatcher,
modalWatcher,
pushNotificationsWatcherSaga,
restoreMnemonicCompleteWatcher,
signWcRequestSaga,
telemetrySaga,
walletConnectSaga,
]
// All monitored sagas must be included here
export const monitoredSagas: Record<string, MonitoredSaga> = {
[createAccountsSagaName]: {
name: createAccountsSagaName,
wrappedSaga: createAccountsSaga,
reducer: createAccountsReducer,
actions: createAccountsActions,
},
[editAccountSagaName]: {
name: editAccountSagaName,
wrappedSaga: editAccountSaga,
reducer: editAccountReducer,
actions: editAccountActions,
},
[swapSagaName]: {
name: swapSagaName,
wrappedSaga: swapSaga,
reducer: swapReducer,
actions: swapActions,
},
[tokenWrapSagaName]: {
name: tokenWrapSagaName,
wrappedSaga: tokenWrapSaga,
reducer: tokenWrapReducer,
actions: tokenWrapActions,
},
}
export const monitoredSagaReducers = getMonitoredSagaReducers(monitoredSagas)
export function* rootMobileSaga() {
// wait until redux-persist has finished rehydration
while (true) {
......
......@@ -637,6 +637,25 @@ const v81SchemaIntermediate = {
delete v81SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount
export const v81Schema = v81SchemaIntermediate
// v82 had a migration but no schema update so skipping it here
export const v83Schema = {
...v81Schema,
pushNotifications: {
generalUpdatesEnabled: true,
priceAlertsEnabled: true,
},
}
const v84SchemaIntermediate = {
...v83Schema,
behaviorHistory: {
...v83Schema.behaviorHistory,
hasViewedWelcomeWalletCard: undefined,
},
}
delete v84SchemaIntermediate.behaviorHistory.hasViewedWelcomeWalletCard
export const v84Schema = v84SchemaIntermediate
// TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer
// export const getSchema = (): RootState => v0Schema
export const getSchema = (): typeof v81Schema => v81Schema
export const getSchema = (): typeof v84Schema => v84Schema
import { BarCodeScanner } from 'expo-barcode-scanner'
import { AutoFocus, BarCodeScanningResult, Camera, CameraType } from 'expo-camera'
import { PermissionStatus } from 'expo-modules-core'
import { PermissionStatus, scanFromURLAsync } from 'expo-barcode-scanner'
import { BarCodeScanningResult, CameraType } from 'expo-camera'
import { CameraProps, CameraView, useCameraPermissions } from 'expo-camera/next'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, LayoutChangeEvent, LayoutRectangle, StyleSheet } from 'react-native'
import DeviceInfo from 'react-native-device-info'
import { launchImageLibrary } from 'react-native-image-picker'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg'
import { DeprecatedButton, Flex, SpinningLoader, Text, ThemeName, TouchableArea, useSporeColors } from 'ui/src'
import { DeprecatedButton, Flex, SpinningLoader, Text, ThemeName, useSporeColors } from 'ui/src'
import CameraScan from 'ui/src/assets/icons/camera-scan.svg'
import { Global, PhotoStacked } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
......@@ -16,9 +16,13 @@ import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { useSporeColorsForTheme } from 'ui/src/hooks/useSporeColors'
import { iconSizes, spacing } from 'ui/src/theme'
import PasteButton from 'uniswap/src/components/buttons/PasteButton'
import { DevelopmentOnly } from 'wallet/src/components/DevelopmentOnly/DevelopmentOnly'
import { logger } from 'utilities/src/logger/logger'
import { openSettings } from 'wallet/src/utils/linking'
enum BarcodeType {
QR = 'qr',
}
type QRCodeScannerProps = {
onScanCode: (data: string) => void
shouldFreezeCamera: boolean
......@@ -48,17 +52,14 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
const dimensions = useDeviceDimensions()
const [permissionResponse, requestPermissionResponse] = Camera.useCameraPermissions()
const permissionStatus = permissionResponse?.status
const [autoFocus, setAutoFocus] = useState(AutoFocus.off)
const [permission, requestPermission] = useCameraPermissions()
const [isReadingImageFile, setIsReadingImageFile] = useState(false)
const [overlayLayout, setOverlayLayout] = useState<LayoutRectangle | null>()
const [infoLayout, setInfoLayout] = useState<LayoutRectangle | null>()
const [bottomLayout, setBottomLayout] = useState<LayoutRectangle | null>()
const handleBarCodeScanned = useCallback(
const handleBarcodeScanned = useCallback(
(result: BarCodeScanningResult): void => {
if (shouldFreezeCamera) {
return
......@@ -89,7 +90,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
return
}
const result = (await BarCodeScanner.scanFromURLAsync(uri, [BarCodeScanner.Constants.BarCodeType.qr]))[0]
const result = (await scanFromURLAsync(uri, [BarcodeType.QR]))[0]
if (!result) {
Alert.alert(t('qrScanner.error.none'))
......@@ -97,15 +98,16 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
return
}
handleBarCodeScanned(result)
}, [handleBarCodeScanned, isReadingImageFile, t])
handleBarcodeScanned(result)
}, [handleBarcodeScanned, isReadingImageFile, t])
useEffect(() => {
const handlePermissionStatus = async (): Promise<void> => {
const cameraState = await requestPermissionResponse()
const latestPermissionStatus = cameraState.status
if ([PermissionStatus.UNDETERMINED, PermissionStatus.DENIED].includes(latestPermissionStatus)) {
if (permission?.granted) {
return
}
const { status } = await requestPermission()
if ([PermissionStatus.UNDETERMINED, PermissionStatus.DENIED].includes(status)) {
Alert.alert(t('qrScanner.error.camera.title'), t('qrScanner.error.camera.message'), [
{ text: t('common.navigation.systemSettings'), onPress: openSettings },
{
......@@ -115,46 +117,36 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
}
}
handlePermissionStatus().catch(() => {})
}, [requestPermissionResponse, t])
handlePermissionStatus().catch((error) => {
logger.error(error, {
tags: { file: 'QRCodeScanner.tsx', function: 'handlePermissionStatus' },
})
})
}, [permission?.granted, t, requestPermission])
const overlayWidth = (overlayLayout?.height ?? 0) / CAMERA_ASPECT_RATIO
const cameraWidth = dimensions.fullWidth
const cameraHeight = CAMERA_ASPECT_RATIO * cameraWidth
const scannerSize = Math.min(overlayWidth, cameraWidth) * SCAN_ICON_WIDTH_RATIO
/**
* Resets the camera auto focus to force the camera to refocus by toggling
* the auto focus off and on. This allows us to manually let the user refocus
* the camera since the expo-camera package does not currently support this.
* The refocus is done by toggling expo-camera's Camera's autoFocus prop. Since
* RN state is batched, we need to debounce the toggle.
*/
function resetCameraAutoFocus(): () => void {
const ARBITRARY_DELAY = 100
const abortController = new AbortController()
setAutoFocus(AutoFocus.off)
setTimeout(() => {
if (!abortController.signal.aborted) {
setAutoFocus(AutoFocus.on)
}
}, ARBITRARY_DELAY)
return () => abortController.abort()
const disableMicPrompt: CameraProps = {
mute: true,
mode: 'picture',
}
return (
<AnimatedFlex grow theme={theme} borderRadius="$rounded12" entering={FadeIn} exiting={FadeOut} overflow="hidden">
<Flex justifyContent="center" style={StyleSheet.absoluteFill}>
<Flex height={cameraHeight} overflow="hidden" width={cameraWidth}>
{permissionStatus === PermissionStatus.GRANTED && !isReadingImageFile && (
<Camera
barCodeScannerSettings={{
barCodeTypes: [BarCodeScanner.Constants.BarCodeType.qr],
{permission?.granted && !isReadingImageFile && (
<CameraView
{...disableMicPrompt}
barcodeScannerSettings={{
barcodeTypes: [BarcodeType.QR],
}}
facing={CameraType.back}
style={StyleSheet.absoluteFillObject}
type={CameraType.back}
autoFocus={autoFocus}
onBarCodeScanned={handleBarCodeScanned}
onBarcodeScanned={handleBarcodeScanned}
/>
)}
</Flex>
......@@ -189,10 +181,8 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
</Text>
</Flex>
{!shouldFreezeCamera ? (
<TouchableArea onPress={resetCameraAutoFocus}>
{/* camera isn't frozen (after seeing barcode) — show the camera scan icon (the four white corners) */}
// camera isn't frozen (after seeing barcode) — show the camera scan icon (the four white corners)
<CameraScan color={colors.white.val} height={scannerSize} strokeWidth={5} width={scannerSize} />
</TouchableArea>
) : (
// camera has been frozen (has seen a barcode) — show the loading spinner and "Connecting..." or "Loading..."
<Flex height={scannerSize} width={scannerSize}>
......@@ -211,9 +201,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
</Flex>
</Flex>
)}
<DevelopmentOnly>
{/* when in development mode AND there's no camera (using iOS Simulator), add a paste button */}
{!shouldFreezeCamera ? (
{DeviceInfo.isEmulatorSync() && !shouldFreezeCamera && (
<Flex centered height={scannerSize} style={[StyleSheet.absoluteFill]} width={scannerSize}>
<Flex
backgroundColor="$surface2"
......@@ -229,9 +217,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
<PasteButton onPress={onScanCode} />
</Flex>
</Flex>
) : null}
</DevelopmentOnly>
)}
<Flex
alignItems="center"
bottom={0}
......
......@@ -85,7 +85,7 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E
<TouchableArea
borderColor={isDarkMode ? '$transparent' : '$surface3'}
borderRadius="$roundedFull"
borderWidth={1}
borderWidth="$spacing1"
p="$spacing16"
paddingEnd="$spacing24"
backgroundColor={colors.DEP_backgroundOverlay.val}
......
......@@ -8,7 +8,7 @@ import { AccountListQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__ge
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { useAccountListData } from 'wallet/src/features/accounts/useAccountListData'
import { Account } from 'wallet/src/features/wallet/accounts/types'
const ADDRESS_ROW_HEIGHT = 40
......@@ -23,7 +23,7 @@ type Portfolio = NonNullable<NonNullable<NonNullable<AccountListQuery['portfolio
function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element {
const { fullHeight } = useDeviceDimensions()
const addresses = useMemo(() => accounts.map((account) => account.address), [accounts])
const { data, loading } = useAccountList({
const { data, loading } = useAccountListData({
addresses,
notifyOnNetworkStatusChange: true,
})
......@@ -57,7 +57,7 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele
<Flex
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
maxHeight={accountsScrollViewHeight}
px="$spacing12"
width="100%"
......
......@@ -74,7 +74,14 @@ export function DappConnectedNetworkModal({ session, onClose }: DappConnectedNet
</Text>
</Flex>
<Flex row>
<Flex grow borderColor="$surface3" borderRadius="$rounded12" borderWidth={1} gap="$spacing16" p="$spacing16">
<Flex
grow
borderColor="$surface3"
borderRadius="$rounded12"
borderWidth="$spacing1"
gap="$spacing16"
p="$spacing16"
>
{session.chains.map((chainId) => (
<Flex key={chainId} row alignItems="center" justifyContent="space-between">
<NetworkLogo chainId={chainId} size={iconSizes.icon24} />
......
......@@ -66,7 +66,7 @@ function KidSuperCheckinModalContent({ request }: { request: SignRequest }): JSX
centered
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
gap="$spacing12"
px="$spacing24"
py="$spacing24"
......
......@@ -144,7 +144,7 @@ function TransactionDetails({
<Flex
borderColor={isLoading ? '$transparent' : '$surface3'}
borderRadius="$rounded12"
borderWidth={1}
borderWidth="$spacing1"
px="$spacing8"
py="$spacing2"
>
......
......@@ -90,7 +90,7 @@ export function WalletConnectRequestModalContent({
<>
<ClientDetails permitInfo={permitInfo} request={request} />
<Flex pt="$spacing8">
<Flex backgroundColor="$surface2" borderColor="$surface3" borderRadius="$rounded16" borderWidth={1}>
<Flex backgroundColor="$surface2" borderColor="$surface3" borderRadius="$rounded16" borderWidth="$spacing1">
{!permitInfo && (
<SectionContainer style={requestMessageStyle}>
<RequestDetails request={request} />
......
......@@ -58,7 +58,7 @@ const SitePermissions = (): JSX.Element => {
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
minHeight={44}
p="$spacing12"
>
......
......@@ -282,7 +282,7 @@ export function WalletConnectModal({
<TouchableArea
borderColor={isDarkMode ? '$transparent' : '$surface3'}
borderRadius="$roundedFull"
borderWidth={1}
borderWidth="$spacing1"
p="$spacing16"
paddingEnd="$spacing24"
backgroundColor={colors.DEP_backgroundOverlay.val}
......
......@@ -3,15 +3,41 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { closeAllModals } from 'src/features/modals/modalSlice'
import { DeprecatedButton, Flex, Text, useSporeColors } from 'ui/src'
import { WalletFilled } from 'ui/src/components/icons'
import { iconSizes, opacify } from 'ui/src/theme'
import { DeprecatedButton, Flex, useSporeColors } from 'ui/src'
import { ArrowDownCircle, WalletFilled } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { spacing } from 'ui/src/theme/spacing'
import { GenericHeader } from 'uniswap/src/components/misc/GenericHeader'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
const CONTAINER_HEIGHT = 160
const OUTER_RING_SIZE = 260
const INNER_RING_SIZE = 175
const SHADOW_RADIUS = 20
const SHADOW_OPACITY = 0.3
const SHADOW_OFFSET = { width: 0, height: 0 } as const
const ICON_OFFSET = -spacing.spacing8
function BackgroundRing({ size }: { size: number }): JSX.Element {
return (
<Flex
position="absolute"
borderRadius="$roundedFull"
borderColor="$surface3"
borderWidth="$spacing1"
height={size}
width={size}
top="50%"
left="50%"
transform={[{ translateX: -size / 2 }, { translateY: -size / 2 }]}
/>
)
}
export function RestoreWalletModal(): JSX.Element | null {
const { t } = useTranslation()
const colors = useSporeColors()
......@@ -29,27 +55,50 @@ export function RestoreWalletModal(): JSX.Element | null {
}
return (
<Modal hideHandlebar backgroundColor={colors.surface2.val} isDismissible={false} name={ModalName.RestoreWallet}>
<Flex centered gap="$spacing16" px="$spacing24" py="$spacing12">
<Modal hideHandlebar backgroundColor={colors.surface1.val} isDismissible={false} name={ModalName.RestoreWallet}>
<Flex centered gap="$spacing24" px="$spacing24" py="$spacing12" backgroundColor="$surface1">
<Flex
centered
borderRadius="$roundedFull"
p="$spacing12"
style={{
backgroundColor: opacify(12, colors.neutral1.val),
}}
width="100%"
height={CONTAINER_HEIGHT}
position="relative"
borderRadius="$rounded16"
borderWidth="$spacing1"
borderColor="$surface3"
backgroundColor="$surface2"
overflow="hidden"
>
<BackgroundRing size={OUTER_RING_SIZE} />
<BackgroundRing size={INNER_RING_SIZE} />
<Flex
centered
borderRadius="$rounded16"
borderWidth="$spacing1"
borderColor="$surface3"
p="$spacing16"
backgroundColor="$surface1"
shadowColor="$accent1"
shadowOffset={SHADOW_OFFSET}
shadowOpacity={SHADOW_OPACITY}
shadowRadius={SHADOW_RADIUS}
>
<WalletFilled color="$neutral1" size={iconSizes.icon24} />
<Flex position="absolute" bottom={ICON_OFFSET} right={ICON_OFFSET}>
<ArrowDownCircle color="$accent1" size={iconSizes.icon24} />
</Flex>
<Text textAlign="center" variant="body1">
{t('account.wallet.button.restore')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t('account.wallet.restore.description')}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing12">
<DeprecatedButton fill testID={TestID.RestoreWallet} theme="primary" onPress={onRestore}>
{t('common.button.restore')}
</Flex>
</Flex>
<GenericHeader
title={t('account.wallet.button.restore')}
titleVariant="subheading1"
subtitle={t('account.wallet.restore.description')}
subtitleVariant="body3"
/>
<Flex row>
<DeprecatedButton fill testID={TestID.RestoreWallet} theme="primary" size="medium" onPress={onRestore}>
{t('common.button.continue')}
</DeprecatedButton>
</Flex>
</Flex>
......
import { NetworkStatus } from '@apollo/client'
import { BottomSheetFlatList } from '@gorhom/bottom-sheet'
import { useFocusEffect } from '@react-navigation/core'
import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation'
......@@ -207,7 +208,7 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
const HeaderComponent = memo(function _HeaderComponent(): JSX.Element | null {
const { t } = useTranslation()
const { balancesById, networkStatus, refetch } = useTokenBalanceListContext()
const hasError = isError(networkStatus, !!balancesById)
const hasError = !!balancesById && networkStatus === NetworkStatus.error
return hasError ? (
<AnimatedFlex entering={FadeInDown} exiting={FadeOut} px="$spacing24" py="$spacing8">
......
......@@ -14,7 +14,7 @@ import * as userSettingsHooks from 'uniswap/src/features/settings/hooks'
import { MobileUserPropertyName } from 'uniswap/src/features/telemetry/user'
// eslint-disable-next-line no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
import * as walletHooks from 'wallet/src/features/wallet/hooks'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
......@@ -33,6 +33,11 @@ jest.mock('wallet/src/features/wallet/Keyring/Keyring', () => {
},
}
})
jest.mock('wallet/src/features/accounts/useAccountListData', () => {
return {
useAccountBalances: jest.fn().mockReturnValue({ totalBalance: 0 }),
}
})
const mockDispatch = jest.fn()
const mockSelector = jest.fn()
......@@ -51,19 +56,28 @@ const signerAccount1 = {
type: AccountType.SignerMnemonic,
address: address1,
timeImportedMs: 100000,
}
pushNotificationsEnabled: true,
mnemonicId: '111',
derivationIndex: 0,
} satisfies SignerMnemonicAccount
const signerAccount2 = {
type: AccountType.SignerMnemonic,
address: address2,
timeImportedMs: 100000,
}
pushNotificationsEnabled: true,
mnemonicId: '222',
derivationIndex: 1,
} satisfies SignerMnemonicAccount
const signerAccount3 = {
type: AccountType.SignerMnemonic,
address: address3,
timeImportedMs: 100000,
}
pushNotificationsEnabled: true,
mnemonicId: '333',
derivationIndex: 2,
} satisfies SignerMnemonicAccount
describe('TraceUserProperties', () => {
afterEach(() => {
......
import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'
import { NativeModules } from 'react-native'
import OneSignal from 'react-native-onesignal'
import { useSelector } from 'react-redux'
import { useBiometricAppSettings, useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks'
import { OneSignalUserTagField } from 'src/features/notifications/constants'
import { getAuthMethod } from 'src/features/telemetry/utils'
import { getFullAppVersion } from 'src/utils/version'
import { useIsDarkMode } from 'ui/src'
......@@ -11,10 +13,11 @@ import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks'
import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks'
import { MobileUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user'
import { isAndroid } from 'utilities/src/platform'
import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors'
// eslint-disable-next-line no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics'
import { useAccountBalances } from 'wallet/src/features/accounts/useAccountListData'
import { useGatingUserPropertyUsernames } from 'wallet/src/features/gating/userPropertyHooks'
import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
import {
......@@ -39,6 +42,12 @@ export function TraceUserProperties(): null {
const hideSmallBalances = useHideSmallBalancesSetting()
const { isTestnetModeEnabled } = useEnabledChains()
const signerAccountAddresses = useMemo(() => signerAccounts.map((account) => account.address), [signerAccounts])
const { totalBalance: signerAccountsTotalBalance } = useAccountBalances({
addresses: signerAccountAddresses,
fetchPolicy: 'cache-first',
})
// Effects must check this and ensure they are setting properties for when analytics is reenabled
const allowAnalytics = useSelector(selectAllowAnalytics)
......@@ -70,12 +79,9 @@ export function TraceUserProperties(): null {
}, [allowAnalytics, isDarkMode])
useEffect(() => {
setUserProperty(MobileUserPropertyName.WalletSignerCount, signerAccounts.length)
setUserProperty(
MobileUserPropertyName.WalletSignerAccounts,
signerAccounts.map((account) => account.address),
)
}, [allowAnalytics, signerAccounts])
setUserProperty(MobileUserPropertyName.WalletSignerCount, signerAccountAddresses.length)
setUserProperty(MobileUserPropertyName.WalletSignerAccounts, signerAccountAddresses)
}, [allowAnalytics, signerAccountAddresses])
useEffect(() => {
setUserProperty(MobileUserPropertyName.WalletViewOnlyCount, viewOnlyAccounts.length)
......@@ -117,5 +123,9 @@ export function TraceUserProperties(): null {
setUserProperty(MobileUserPropertyName.TestnetModeEnabled, isTestnetModeEnabled)
}, [allowAnalytics, isTestnetModeEnabled])
useEffect(() => {
OneSignal.sendTag(OneSignalUserTagField.AccountIsUnfunded, signerAccountsTotalBalance === 0 ? 'true' : 'false')
}, [signerAccountsTotalBalance])
return null
}
......@@ -2,11 +2,11 @@ import { AccountCardItem } from 'src/components/accounts/AccountCardItem'
import { fireEvent, render, screen, waitFor } from 'src/test/test-utils'
import { ON_PRESS_EVENT_PAYLOAD, SAMPLE_SEED_ADDRESS_1, amount, portfolio } from 'uniswap/src/test/fixtures'
import { queryResolvers } from 'uniswap/src/test/utils'
import * as hooks from 'wallet/src/features/accounts/hooks'
import * as hooks from 'wallet/src/features/accounts/useAccountListData'
describe(AccountCardItem, () => {
beforeEach(() => {
jest.spyOn(hooks, 'useAccountList').mockReturnValue({
jest.spyOn(hooks, 'useAccountListData').mockReturnValue({
data: undefined,
loading: false,
networkStatus: 7,
......
......@@ -18,7 +18,7 @@ import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { useAccountListData } from 'wallet/src/features/accounts/useAccountListData'
type AccountCardItemProps = {
address: Address
......@@ -44,7 +44,7 @@ function PortfolioValue({
// Since we're adding a new wallet address to the `ownerAddresses` array, this will be a brand new query, which won't be cached.
// To avoid all wallets showing a "loading" state, we read directly from cache while we wait for the other query to complete.
const { data } = useAccountList({
const { data } = useAccountListData({
fetchPolicy: 'cache-first',
addresses: [address],
})
......
......@@ -10,7 +10,7 @@ import { PollingInterval } from 'uniswap/src/constants/misc'
import { AccountType } from 'uniswap/src/features/accounts/types'
import { useAsyncData } from 'utilities/src/react/hooks'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { useAccountListData } from 'wallet/src/features/accounts/useAccountListData'
import { Account } from 'wallet/src/features/wallet/accounts/types'
type AccountListProps = Pick<ComponentProps<typeof AccountCardItem>, 'onPress'> & {
......@@ -63,7 +63,7 @@ export function AccountList({ accounts, onPress, isVisible }: AccountListProps):
const colors = useSporeColors()
const addresses = useMemo(() => accounts.map((a) => a.address), [accounts])
const { data, networkStatus, refetch, startPolling, stopPolling } = useAccountList({
const { data, networkStatus, refetch, startPolling, stopPolling } = useAccountListData({
addresses,
notifyOnNetworkStatusChange: true,
})
......
......@@ -27,6 +27,7 @@ exports[`AccountList renders without error 1`] = `
"derivationIndex": 0,
"mnemonicId": "0x82D56A352367453f74FC0dC7B071b311da373Fa6",
"name": "Test Account",
"pushNotificationsEnabled": true,
"timeImportedMs": 10,
"type": "signerMnemonic",
},
......
......@@ -33,7 +33,7 @@ export function BottomBanner({ text, icon, backgroundColor, translateY }: Bottom
backgroundColor={backgroundColor ? backgroundColor : '$accent1'}
borderColor="$surface3"
borderRadius="$rounded8"
borderWidth={1}
borderWidth="$spacing1"
bottom={0}
entering={FadeIn}
exiting={FadeOut}
......
......@@ -208,7 +208,7 @@ function AllNetworksPill({ onPress, selected }: { onPress: () => void; selected:
backgroundColor={selected ? '$surface3' : '$surface1'}
borderColor="$surface3"
borderRadius="$rounded12"
borderWidth={1}
borderWidth="$spacing1"
gap="$spacing8"
pl="$spacing4"
pr="$spacing12"
......
......@@ -165,7 +165,7 @@ function ServiceProviderLogo({ uri }: { uri: string }): JSX.Element {
backgroundColor="$surface1"
borderColor="$surface1"
borderRadius="$rounded8"
borderWidth={2}
borderWidth="$spacing2"
overflow="hidden"
>
<ImageUri
......@@ -187,7 +187,7 @@ function ReceiveCryptoIcon(): JSX.Element {
backgroundColor="$surface1"
borderColor="$surface1"
borderRadius="$roundedFull"
borderWidth={1}
borderWidth="$spacing1"
overflow="hidden"
>
<ArrowDownCircle color="$accent1" size="$icon.24" />
......
......@@ -5,11 +5,18 @@ import { useDispatch, useSelector } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal'
import { openModal } from 'src/features/modals/modalSlice'
import {
NotificationPermission,
useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { Flex } from 'ui/src'
import { Buy, ShieldCheck, UniswapLogo } from 'ui/src/components/icons'
import { PUSH_NOTIFICATIONS_CARD_BANNER } from 'ui/src/assets'
import { Buy, ShieldCheck } from 'ui/src/components/icons'
import { UnichainIntroModal } from 'uniswap/src/components/unichain/UnichainIntroModal'
import { AccountType } from 'uniswap/src/features/accounts/types'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
......@@ -25,8 +32,8 @@ import {
import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack'
import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { selectHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/slice'
import { selectHasViewedNotificationsCard } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasViewedNotificationsCard } from 'wallet/src/features/behaviorHistory/slice'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
type OnboardingIntroCardStackProps = {
......@@ -44,8 +51,13 @@ export function OnboardingIntroCardStack({
const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic
const hasBackups = activeAccount.backups && activeAccount.backups.length > 0
const welcomeCardTitle = t('onboarding.home.intro.welcome.title')
const hasViewedWelcomeWalletCard = useSelector(selectHasViewedWelcomeWalletCard)
const { notificationPermissionsEnabled } = useNotificationOSPermissionsEnabled()
const notificationOnboardingCardEnabled = useFeatureFlag(FeatureFlags.NotificationOnboardingCard)
const hasViewedNotificationsCard = useSelector(selectHasViewedNotificationsCard)
const showEnableNotificationsCard =
notificationOnboardingCardEnabled &&
notificationPermissionsEnabled === NotificationPermission.Disabled &&
!hasViewedNotificationsCard
const { navigateToSwapFlow } = useWalletNavigation()
......@@ -128,28 +140,30 @@ export function OnboardingIntroCardStack({
output.push(...sharedCards)
if (output.length && !hasViewedWelcomeWalletCard) {
output.unshift({
loggingName: OnboardingCardLoggingName.WelcomeWallet,
if (showEnableNotificationsCard) {
output.push({
loggingName: OnboardingCardLoggingName.EnablePushNotifications,
graphic: {
type: IntroCardGraphicType.Icon,
Icon: UniswapLogo,
iconProps: {
color: '$accent1',
type: IntroCardGraphicType.Image,
image: PUSH_NOTIFICATIONS_CARD_BANNER,
},
iconContainerProps: {
backgroundColor: '$accent2',
borderRadius: '$rounded12',
title: t('onboarding.home.intro.pushNotifications.title'),
description: t('onboarding.home.intro.pushNotifications.description'),
cardType: CardType.Dismissible,
onPress: (): void => {
navigate(ModalName.NotificationsOSSettings)
dispatch(setHasViewedNotificationsCard(true))
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
element: ElementName.OnboardingIntroCardEnablePushNotifications,
})
},
onClose: (): void => {
dispatch(setHasViewedNotificationsCard(true))
},
title: welcomeCardTitle,
description: t('onboarding.home.intro.welcome.description'),
cardType: CardType.Swipe,
})
}
return output
}, [hasBackups, showEmptyWalletState, hasViewedWelcomeWalletCard, isSignerAccount, sharedCards, t, welcomeCardTitle])
}, [hasBackups, showEmptyWalletState, isSignerAccount, sharedCards, t, showEnableNotificationsCard, dispatch])
const handleSwiped = useCallback(
(_card: IntroCardProps, index: number) => {
......@@ -159,12 +173,8 @@ export function OnboardingIntroCardStack({
card_name: loggingName,
})
}
if (!hasViewedWelcomeWalletCard && cards[index]?.title === welcomeCardTitle) {
dispatch(setHasViewedWelcomeWalletCard(true))
}
},
[cards, dispatch, hasViewedWelcomeWalletCard, welcomeCardTitle],
[cards],
)
const UnichainIntroModalInstance = useMemo((): JSX.Element => {
......
......@@ -24,7 +24,7 @@ export const PasswordInput = forwardRef<NativeTextInput, TextInputProps>(functio
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
p="$spacing4"
>
<AnimatedFlex fill grow row alignItems="center" minHeight={48}>
......@@ -34,7 +34,7 @@ export const PasswordInput = forwardRef<NativeTextInput, TextInputProps>(functio
autoCorrect={false}
backgroundColor="$transparent"
blurOnSubmit={false}
borderWidth={0}
borderWidth="$none"
clearTextOnFocus={false}
flex={1}
fontFamily="$subHeading"
......
......@@ -20,7 +20,7 @@ export function SelectionCircle({
centered
borderColor={selected ? selectedColor : unselectedColor}
borderRadius="$roundedFull"
borderWidth={1}
borderWidth="$spacing1"
height={iconSizes[size]}
width={iconSizes[size]}
>
......
......@@ -24,7 +24,7 @@ export function HiddenMnemonicWordView({
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
gap="$spacing36"
px="$spacing32"
py="$spacing24"
......@@ -41,7 +41,7 @@ export function HiddenMnemonicWordView({
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
borderWidth="$spacing1"
gap="$spacing4"
paddingEnd="$spacing16"
paddingStart="$spacing12"
......
......@@ -17,7 +17,7 @@ function _NotificationBadge({ children, address }: Props): JSX.Element {
backgroundColor="$accent1"
borderColor="$surface2"
borderRadius="$roundedFull"
borderWidth={2}
borderWidth="$spacing2"
height={NOTIFICATION_DOT_SIZE}
position="absolute"
right={-NOTIFICATION_DOT_SIZE / 4}
......
......@@ -36,8 +36,6 @@ function* _handleOffRampReturnLink(url: URL) {
throw new Error('Missing externalTransactionId or moonpay data in fiat offramp deep link')
}
sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampWidgetCompleted, { externalTransactionId })
let offRampTransferDetails: OffRampTransferDetailsResponse | undefined
try {
......@@ -51,6 +49,7 @@ function* _handleOffRampReturnLink(url: URL) {
} catch (error) {
logger.error(error, {
tags: { file: 'handleOffRampReturnLinkSaga', function: 'handleOffRampReturnLink' },
extra: { url: url.toString() },
})
throw new Error('Failed to fetch offramp transfer details')
}
......@@ -62,6 +61,16 @@ function* _handleOffRampReturnLink(url: URL) {
const { tokenAddress, baseCurrencyCode, baseCurrencyAmount, depositWalletAddress, logos, provider, chainId } =
offRampTransferDetails
const analyticsProperties = {
cryptoCurrency: baseCurrencyCode,
currencyAmount: baseCurrencyAmount,
serviceProvider: provider,
chainId,
externalTransactionId,
}
sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampWidgetCompleted, analyticsProperties)
const currencyTradeableAsset: TradeableAsset = {
address: tokenAddress,
chainId: Number(chainId) as UniverseChainId,
......@@ -71,14 +80,8 @@ function* _handleOffRampReturnLink(url: URL) {
const fiatOffRampMetaData: FiatOffRampMetaData = {
name: provider,
logoUrl: logos.lightLogo,
onSubmitCallback: () => {
sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampFundsSent, {
cryptoCurrency: baseCurrencyCode,
currencyAmount: baseCurrencyAmount,
serviceProvider: provider,
chainId,
externalTransactionId,
})
onSubmitCallback: (amountUSD?: number) => {
sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampFundsSent, { ...analyticsProperties, amountUSD })
},
moonpayCurrencyCode: baseCurrencyCode,
meldCurrencyCode: baseCurrencyCode,
......
......@@ -217,7 +217,7 @@ export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHea
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
height={46}
p="$spacing12"
shadowColor="$neutral1"
......@@ -232,7 +232,7 @@ export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHea
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
height={46}
justifyContent="center"
px="$spacing12"
......
......@@ -234,7 +234,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
autoFocus
alignSelf="stretch"
backgroundColor="$transparent"
borderWidth={0}
borderWidth="$none"
disabled={disabled}
fiatCurrencyInfo={fiatCurrencyInfo}
fontFamily="$heading"
......
......@@ -122,7 +122,7 @@ export function GenericImportForm({
backgroundColor="$surface1"
borderColor={borderColor}
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
minHeight={shouldUseMinHeight ? INPUT_MIN_HEIGHT : undefined}
px="$spacing24"
py="$spacing16"
......
import { Linking } from 'react-native'
import OneSignal, { NotificationReceivedEvent, OpenedEvent } from 'react-native-onesignal'
import { NotificationType } from 'src/features/notifications/constants'
import { config } from 'uniswap/src/config'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { getFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/gating/hooks'
import { GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE } from 'uniswap/src/features/portfolio/portfolioUpdates/constants'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
import { isIOS } from 'utilities/src/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient'
......@@ -10,8 +15,34 @@ export const initOneSignal = (): void => {
OneSignal.setAppId(config.onesignalAppId)
OneSignal.setNotificationWillShowInForegroundHandler((event: NotificationReceivedEvent) => {
const notification = event.getNotification()
const additionalData = notification.additionalData as { notification_type?: string }
const notificationType = additionalData?.notification_type
let enabled = false
// Some special notif filtering logic is needed for iOS, avoiding exposure
if (isIOS) {
switch (notificationType) {
case NotificationType.UnfundedWalletReminder:
enabled = getFeatureFlagWithExposureLoggingDisabled(FeatureFlags.NotificationUnfundedWallets)
break
case NotificationType.PriceAlert:
enabled = getFeatureFlagWithExposureLoggingDisabled(FeatureFlags.NotificationPriceAlerts)
break
default:
enabled = false
}
} else {
if (
notificationType === NotificationType.UnfundedWalletReminder ||
notificationType === NotificationType.PriceAlert
) {
enabled = true
}
}
// Complete with undefined means don't show OS notifications while app is in foreground
event.complete()
event.complete(enabled ? notification : undefined)
})
OneSignal.setNotificationOpenedHandler((event: OpenedEvent) => {
......@@ -32,6 +63,21 @@ export const initOneSignal = (): void => {
Linking.emit('url', { url: event.notification.launchURL })
}
})
getUniqueId()
.then((deviceId) => {
if (deviceId) {
OneSignal.setExternalUserId(deviceId)
}
})
.catch(() =>
logger.error('Failed to get device ID for OneSignal', {
tags: {
file: 'Onesignal.ts',
function: 'initOneSignal',
},
}),
)
}
export const promptPushPermission = async (): Promise<boolean> => {
......
// Enum value represents tag name in OneSignal
export enum NotifSettingType {
GeneralUpdates = 'settings_general_updates_enabled',
PriceAlerts = 'settings_price_alerts_enabled',
}
// Enum value represents tag name in OneSignal
export enum OneSignalUserTagField {
OnboardingCompletedAt = 'onboarding_completed_at',
OnboardingImportType = 'onboarding_import_type',
OnboardingWalletAddress = 'onboarding_wallet_address',
SwapLastCompletedAt = 'swap_last_completed_at',
AccountIsUnfunded = 'account_is_unfunded',
GatingUnfundedWalletsEnabled = 'gating_unfunded_wallets_enabled',
GatingPriceAlertsEnabled = 'gating_price_alerts_enabled',
}
export enum NotificationType {
UnfundedWalletReminder = 'unfunded_wallet_reminder',
PriceAlert = 'price_alert',
}
import { useMutation } from '@tanstack/react-query'
import { useCallback, useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { promptPushPermission } from 'src/features/notifications/Onesignal'
import { NotifSettingType } from 'src/features/notifications/constants'
import {
NotificationPermission,
useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { NotifSettingType, getNotifSetting, handleNotifSettingToggled } from 'src/features/notifications/settings'
import { selectAllPushNotificationSettings } from 'src/features/notifications/selectors'
import { updateNotifSettings } from 'src/features/notifications/slice'
import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks'
......@@ -55,21 +57,21 @@ export function useSettingNotificationToggle({
type: NotifSettingType
onToggle?: (enabled: boolean) => void
}): ReturnType<typeof useBaseNotificationToggle> {
const [isAppPermissionEnabled, setAppPermissionEnabled] = useState(false)
const dispatch = useDispatch()
const { generalUpdatesEnabled, priceAlertsEnabled } = useSelector(selectAllPushNotificationSettings)
useEffect(() => {
getNotifSetting(type)
.then(setAppPermissionEnabled)
.catch(() => {})
}, [type])
const permissionEnabledMap: Record<NotifSettingType, boolean> = {
[NotifSettingType.GeneralUpdates]: generalUpdatesEnabled,
[NotifSettingType.PriceAlerts]: priceAlertsEnabled,
}
const isAppPermissionEnabled = permissionEnabledMap[type]
const handleToggle = useCallback(
(enabled: boolean) => {
handleNotifSettingToggled(type, enabled)
setAppPermissionEnabled(enabled)
dispatch(updateNotifSettings({ [type]: enabled }))
onToggle?.(enabled)
},
[onToggle, type],
[dispatch, onToggle, type],
)
return useBaseNotificationToggle({ isAppPermissionEnabled, onToggle: handleToggle })
......
import OneSignal from 'react-native-onesignal'
import { NotifSettingType, OneSignalUserTagField } from 'src/features/notifications/constants'
import { selectAllPushNotificationSettings } from 'src/features/notifications/selectors'
import { initNotifsForNewUser, updateNotifSettings } from 'src/features/notifications/slice'
import { call, select, takeEvery } from 'typed-redux-saga'
import { finalizeTransaction } from 'uniswap/src/features/transactions/slice'
import { TransactionStatus, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors'
export function* pushNotificationsWatcherSaga() {
yield* call(syncWithOneSignal)
yield* takeEvery(initNotifsForNewUser.type, initNewUser)
yield* takeEvery(updateNotifSettings.type, syncWithOneSignal)
yield* takeEvery(finalizeTransaction.type, processFinalizedTx)
}
/**
* Due to our app not having an account abstraction, OneSignal values are device-specific.
* So, this is intentionally driving local changes as the source of truth,
* since OneSignal is not a fully reliable and scalable backend.
* If we ever need to share settings across devices, this will need to change.
*/
function* syncWithOneSignal() {
const finishedOnboarding = yield* select(selectFinishedOnboarding)
if (finishedOnboarding) {
const { generalUpdatesEnabled, priceAlertsEnabled } = yield* select(selectAllPushNotificationSettings)
yield* call(OneSignal.sendTags, {
[NotifSettingType.GeneralUpdates]: generalUpdatesEnabled.toString(),
[NotifSettingType.PriceAlerts]: priceAlertsEnabled.toString(),
})
}
}
function* initNewUser() {
yield* call(OneSignal.sendTags, {
[NotifSettingType.GeneralUpdates]: 'true',
[NotifSettingType.PriceAlerts]: 'true',
})
}
function* processFinalizedTx(action: ReturnType<typeof finalizeTransaction>) {
const isSuccessfulSwap =
action.payload.typeInfo.type === TransactionType.Swap && action.payload.status === TransactionStatus.Success
if (isSuccessfulSwap) {
yield* call(
OneSignal.sendTag,
OneSignalUserTagField.SwapLastCompletedAt,
Math.floor(Date.now() / ONE_SECOND_MS).toString(),
)
}
}
import { MobileState } from 'src/app/mobileReducer'
export const selectGeneralUpdatesEnabled = (state: MobileState): boolean =>
state.pushNotifications.generalUpdatesEnabled
export const selectPriceAlertsEnabled = (state: MobileState): boolean => state.pushNotifications.priceAlertsEnabled
export const selectAllPushNotificationSettings = (
state: MobileState,
): {
generalUpdatesEnabled: boolean
priceAlertsEnabled: boolean
} => {
const { generalUpdatesEnabled, priceAlertsEnabled } = state.pushNotifications
return { generalUpdatesEnabled, priceAlertsEnabled }
}
import OneSignal from 'react-native-onesignal'
// Enum value represents tag name in OneSignal
export enum NotifSettingType {
GeneralUpdates = 'settings_general_updates_enabled',
PriceAlerts = 'settings_price_alerts_enabled',
}
export function handleNotifSettingToggled(type: NotifSettingType, enabled: boolean): void {
OneSignal.sendTag(type, enabled ? 'true' : 'false')
}
export async function getNotifSetting(type: NotifSettingType): Promise<boolean> {
return new Promise((resolve, _reject) => {
OneSignal.getTags((tags) => resolve(tags?.[type] === 'true'))
})
}
import { PayloadAction, createSlice } from '@reduxjs/toolkit'
import { NotifSettingType } from 'src/features/notifications/constants'
export interface PushNotificationsState {
generalUpdatesEnabled: boolean
priceAlertsEnabled: boolean
}
export const initialPushNotificationsState: PushNotificationsState = {
generalUpdatesEnabled: true,
priceAlertsEnabled: true,
}
export type SettingsUpdatePayload = {
[k in NotifSettingType]?: boolean
}
const slice = createSlice({
name: 'pushNotifications',
initialState: initialPushNotificationsState,
reducers: {
updateNotifSettings: (state, action: PayloadAction<SettingsUpdatePayload>) => {
if (action.payload[NotifSettingType.GeneralUpdates] !== undefined) {
state.generalUpdatesEnabled = action.payload[NotifSettingType.GeneralUpdates]
}
if (action.payload[NotifSettingType.PriceAlerts] !== undefined) {
state.priceAlertsEnabled = action.payload[NotifSettingType.PriceAlerts]
}
},
initNotifsForNewUser: (state) => {
// Primary used to trigger side effects in saga
state.generalUpdatesEnabled = true
state.priceAlertsEnabled = true
},
},
})
export const { initNotifsForNewUser, updateNotifSettings } = slice.actions
export const pushNotificationsReducer = slice.reducer
......@@ -102,7 +102,7 @@ function CloudBackupPreview(): JSX.Element {
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded12"
borderWidth={1}
borderWidth="$spacing1"
gap="$spacing16"
px="$spacing12"
py="$spacing8"
......
......@@ -30,7 +30,7 @@ export function LockPreviewImage({ height = DEFAULT_PREVIEW_HEIGHT }: { height?:
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded12"
borderWidth={1}
borderWidth="$spacing1"
height={BOXES_CONTAINER_HEIGHT}
position="relative"
pt="$spacing16"
......@@ -58,7 +58,7 @@ export function LockPreviewImage({ height = DEFAULT_PREVIEW_HEIGHT }: { height?:
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded12"
borderWidth={1}
borderWidth="$spacing1"
p="$spacing12"
top="$spacing24"
>
......
......@@ -34,7 +34,7 @@ export function OptionCard({
backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
disabled={disabled}
opacity={disabled ? 0.5 : opacity}
p="$spacing16"
......
import { SharedEventName } from '@uniswap/analytics-events'
import OneSignal from 'react-native-onesignal'
import { useDispatch } from 'react-redux'
import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app/navigation/types'
import { OneSignalUserTagField } from 'src/features/notifications/constants'
import { initNotifsForNewUser } from 'src/features/notifications/slice'
import { MobileAppsFlyerEvents } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent, sendAppsFlyerEvent } from 'uniswap/src/features/telemetry/send'
import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice'
......@@ -25,12 +29,20 @@ export function useCompleteOnboardingCallback({
const navigation = useOnboardingStackNavigation()
const onboardingAccounts = getAllOnboardingAccounts()
const onboardingAddresses = Object.keys(onboardingAccounts)
const onboardingAddresses = onboardingAccounts.map((account) => account.address)
return async () => {
// Run all shared onboarding completion logic
await finishOnboarding({ importType })
// Initializes notification settings
dispatch(initNotifsForNewUser())
OneSignal.sendTags({
[OneSignalUserTagField.OnboardingWalletAddress]: onboardingAddresses[0] ?? '',
[OneSignalUserTagField.OnboardingCompletedAt]: Math.floor(Date.now() / ONE_SECOND_MS).toString(),
[OneSignalUserTagField.OnboardingImportType]: importType,
})
// Send appsflyer event for mobile attribution
if (entryPoint === OnboardingEntryPoint.FreshInstallOrReplace) {
sendAppsFlyerEvent(MobileAppsFlyerEvents.OnboardingCompleted, { importType }).catch((error) =>
......
......@@ -82,7 +82,7 @@ export function AIAssistantScreen(): JSX.Element {
backgroundColor="$surface1"
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth={1}
borderWidth="$spacing1"
mx="$spacing16"
>
<Input
......
import { useEffect, useState } from 'react'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
/**
* Dynamically imported AIAssistantOverlay to allow for dev testing without
* adding the openai package in production.
*/
export function DevAIAssistantOverlay(): JSX.Element | null {
const openAIAssistantEnabled = useFeatureFlag(FeatureFlags.OpenAIAssistant)
const [Component, setComponent] = useState<React.FC | null>(null)
const enabled = __DEV__ && openAIAssistantEnabled
useEffect(() => {
if (enabled) {
const getComponent = async (): Promise<void> => {
const { AIAssistantOverlay } = await import('src/features/openai/AIAssistantOverlay')
setComponent((): React.FC => AIAssistantOverlay)
}
getComponent().catch(() => {})
}
}, [enabled])
if (!enabled) {
return null
}
return Component ? <Component /> : null
}
import { useEffect, useState } from 'react'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
/**
* Dynamically imported AIAssistantScreen to allow for dev testing without
* adding the openai package in production.
*/
export function DevAIAssistantScreen(): JSX.Element | null {
const openAIAssistantEnabled = useFeatureFlag(FeatureFlags.OpenAIAssistant)
const [Component, setComponent] = useState<React.FC | null>(null)
const enabled = __DEV__ && openAIAssistantEnabled
useEffect(() => {
if (enabled) {
const getComponent = async (): Promise<void> => {
const { AIAssistantScreen } = await import('src/features/openai/AIAssistantScreen')
setComponent((): React.FC => AIAssistantScreen)
}
getComponent().catch(() => {})
}
}, [enabled])
if (!openAIAssistantEnabled) {
return null
}
return Component ? <Component /> : null
}
import { PropsWithChildren, ReactNode, useEffect, useState } from 'react'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
type ProviderComponentType = ({ children }: { children: ReactNode }) => JSX.Element
/**
* Dynamically imported OpenAIProvider to allow for dev testing without
* adding the openai package in production.
*/
export const DevOpenAIProvider = ({ children }: PropsWithChildren): JSX.Element => {
const openAIAssistantEnabled = useFeatureFlag(FeatureFlags.OpenAIAssistant)
const [OpenAIProvider, setOpenAIProvider] = useState<ProviderComponentType>()
const enabled = __DEV__ && openAIAssistantEnabled
useEffect(() => {
if (enabled) {
const getComponent = async (): Promise<void> => {
const { OpenAIContextProvider } = await import('src/features/openai/OpenAIContext')
setOpenAIProvider((): ProviderComponentType => OpenAIContextProvider)
}
getComponent().catch(() => {})
}
}, [enabled])
if (!OpenAIProvider) {
return <>{children}</>
}
return <OpenAIProvider>{children}</OpenAIProvider>
}
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