ci(release): publish latest release

parent f9a2e2e2
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmNy6ppw64jmLBEZ6r8D19beUVH3objJPrjMfNxvugqakD` - CIDv0: `Qma8zLjgStpH8bA8CcnrsPszS38XG4D9YqdW39VgnUSF64`
- CIDv1: `bafybeiajkzyd25iwsu5lax4wtilh5ukji3kmzrt7r76k45pcminbsirsty` - CIDv1: `bafybeifpj54iwonrxyywl5g5crbd7krugzmgsr4lw6txrkygunexey66su`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). 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. ...@@ -10,14 +10,71 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeiajkzyd25iwsu5lax4wtilh5ukji3kmzrt7r76k45pcminbsirsty.ipfs.dweb.link/ - https://bafybeifpj54iwonrxyywl5g5crbd7krugzmgsr4lw6txrkygunexey66su.ipfs.dweb.link/
- [ipfs://QmNy6ppw64jmLBEZ6r8D19beUVH3objJPrjMfNxvugqakD/](ipfs://QmNy6ppw64jmLBEZ6r8D19beUVH3objJPrjMfNxvugqakD/) - [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 ### 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 web/5.69.0
\ No newline at end of file \ No newline at end of file
...@@ -63,7 +63,7 @@ function PopupContent(): JSX.Element { ...@@ -63,7 +63,7 @@ function PopupContent(): JSX.Element {
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius={6} borderRadius={6}
borderWidth={1} borderWidth="$spacing1"
bottom={-spacing.spacing4} bottom={-spacing.spacing4}
p="$spacing2" p="$spacing2"
position="absolute" position="absolute"
......
import '@tamagui/core/reset.css' import '@tamagui/core/reset.css'
import 'src/app/Global.css' import 'src/app/Global.css'
import { SharedEventName } from '@uniswap/analytics-events'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
...@@ -236,10 +237,11 @@ export default function SidebarApp(): JSX.Element { ...@@ -236,10 +237,11 @@ export default function SidebarApp(): JSX.Element {
}, []) }, [])
const isLoggedIn = useIsWalletUnlocked() const isLoggedIn = useIsWalletUnlocked()
const hasSentLoginEvent = useRef(false) const hasSentAppLoadEvent = useRef(false)
useEffect(() => { useEffect(() => {
if (isLoggedIn !== null && !hasSentLoginEvent.current) { if (isLoggedIn !== null && !hasSentAppLoadEvent.current) {
hasSentLoginEvent.current = true hasSentAppLoadEvent.current = true
sendAnalyticsEvent(SharedEventName.APP_LOADED)
sendAnalyticsEvent(ExtensionEventName.SidebarLoad, { locked: !isLoggedIn }) sendAnalyticsEvent(ExtensionEventName.SidebarLoad, { locked: !isLoggedIn })
} }
}, [isLoggedIn]) }, [isLoggedIn])
......
...@@ -21,7 +21,7 @@ export const Input = forwardRef<Input, InputProps>(function _Input( ...@@ -21,7 +21,7 @@ export const Input = forwardRef<Input, InputProps>(function _Input(
backgroundColor={large ? '$surface1' : '$surface2'} backgroundColor={large ? '$surface1' : '$surface2'}
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
focusStyle={inputStyles.inputFocus} focusStyle={inputStyles.inputFocus}
fontSize={fonts.subheading2.fontSize} fontSize={fonts.subheading2.fontSize}
height="auto" height="auto"
......
...@@ -51,7 +51,7 @@ export const MnemonicViewer = ({ mnemonic }: { mnemonic?: string[] }): JSX.Eleme ...@@ -51,7 +51,7 @@ export const MnemonicViewer = ({ mnemonic }: { mnemonic?: string[] }): JSX.Eleme
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
gap="$spacing12" gap="$spacing12"
position="relative" position="relative"
px={px} px={px}
......
...@@ -19,7 +19,7 @@ function WalletSkeleton({ opacity }: { opacity: number }): JSX.Element { ...@@ -19,7 +19,7 @@ function WalletSkeleton({ opacity }: { opacity: number }): JSX.Element {
alignItems="center" alignItems="center"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
height={WALLET_PREVIEW_CARD_HEIGHT} height={WALLET_PREVIEW_CARD_HEIGHT}
justifyContent="flex-start" justifyContent="flex-start"
opacity={opacity} opacity={opacity}
......
...@@ -84,7 +84,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -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" 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 <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" data-testid="account-icon"
> >
<svg <svg
...@@ -205,7 +205,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -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" 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 <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 <svg
fill="none" fill="none"
...@@ -316,7 +316,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -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" 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 <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" data-testid="account-icon"
> >
<svg <svg
...@@ -437,7 +437,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -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" 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 <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 <svg
fill="none" fill="none"
......
import { useSortedAccountList } from 'src/app/features/accounts/useSortedAccountList' import { useSortedAccountList } from 'src/app/features/accounts/useSortedAccountList'
import { act, renderHook } from 'src/test/test-utils' 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') jest.mock('wallet/src/features/accounts/useAccountListData')
const mockUseAccountList = useAccountList as jest.MockedFunction<typeof useAccountList> const mockUseAccountList = useAccountListData as jest.MockedFunction<typeof useAccountListData>
describe('useSortedAccountList', () => { describe('useSortedAccountList', () => {
beforeEach(() => { beforeEach(() => {
......
import { useMemo } from 'react' import { useMemo } from 'react'
import { usePrevious } from 'utilities/src/react/hooks' 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 { interface AddressWithBalance {
address: Address address: Address
...@@ -8,7 +8,7 @@ interface AddressWithBalance { ...@@ -8,7 +8,7 @@ interface AddressWithBalance {
} }
export function useSortedAccountList(addresses: Address[]): AddressWithBalance[] { export function useSortedAccountList(addresses: Address[]): AddressWithBalance[] {
const { data: accountBalanceData } = useAccountList({ const { data: accountBalanceData } = useAccountListData({
addresses, addresses,
}) })
......
...@@ -45,6 +45,7 @@ interface DappRequestFooterProps { ...@@ -45,6 +45,7 @@ interface DappRequestFooterProps {
showNetworkCost?: boolean showNetworkCost?: boolean
transactionGasFeeResult?: GasFeeResult transactionGasFeeResult?: GasFeeResult
isUniswapX?: boolean isUniswapX?: boolean
disableConfirm?: boolean
} }
type DappRequestContentProps = DappRequestHeaderProps & DappRequestFooterProps type DappRequestContentProps = DappRequestHeaderProps & DappRequestFooterProps
...@@ -88,6 +89,7 @@ export function DappRequestContent({ ...@@ -88,6 +89,7 @@ export function DappRequestContent({
transactionGasFeeResult, transactionGasFeeResult,
children, children,
isUniswapX, isUniswapX,
disableConfirm,
}: PropsWithChildren<DappRequestContentProps>): JSX.Element { }: PropsWithChildren<DappRequestContentProps>): JSX.Element {
const { forwards, currentIndex } = useDappRequestQueueContext() const { forwards, currentIndex } = useDappRequestQueueContext()
...@@ -108,6 +110,7 @@ export function DappRequestContent({ ...@@ -108,6 +110,7 @@ export function DappRequestContent({
showAllNetworks={showAllNetworks} showAllNetworks={showAllNetworks}
showNetworkCost={showNetworkCost} showNetworkCost={showNetworkCost}
transactionGasFeeResult={transactionGasFeeResult} transactionGasFeeResult={transactionGasFeeResult}
disableConfirm={disableConfirm}
onCancel={onCancel} onCancel={onCancel}
onConfirm={onConfirm} onConfirm={onConfirm}
/> />
...@@ -162,6 +165,7 @@ export function DappRequestFooter({ ...@@ -162,6 +165,7 @@ export function DappRequestFooter({
showNetworkCost, showNetworkCost,
transactionGasFeeResult, transactionGasFeeResult,
isUniswapX, isUniswapX,
disableConfirm,
}: DappRequestFooterProps): JSX.Element { }: DappRequestFooterProps): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const activeAccount = useActiveAccountWithThrow() const activeAccount = useActiveAccountWithThrow()
...@@ -253,7 +257,7 @@ export function DappRequestFooter({ ...@@ -253,7 +257,7 @@ export function DappRequestFooter({
{t('common.button.cancel')} {t('common.button.cancel')}
</DeprecatedButton> </DeprecatedButton>
<DeprecatedButton <DeprecatedButton
disabled={!isConfirmEnabled} disabled={!isConfirmEnabled || disableConfirm}
flex={1} flex={1}
flexBasis={1} flexBasis={1}
size="medium" size="medium"
......
...@@ -85,7 +85,10 @@ function* dappRequestApproval({ ...@@ -85,7 +85,10 @@ function* dappRequestApproval({
if (!senderTabId) { if (!senderTabId) {
throw new Error('senderTabId is required') 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') throw new Error('requestId is required')
} }
......
...@@ -70,7 +70,7 @@ export function FallbackEthSendRequestContent({ ...@@ -70,7 +70,7 @@ export function FallbackEthSendRequestContent({
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
gap="$spacing12" gap="$spacing12"
p="$spacing16" p="$spacing16"
width="100%" width="100%"
...@@ -100,7 +100,7 @@ export function FallbackEthSendRequestContent({ ...@@ -100,7 +100,7 @@ export function FallbackEthSendRequestContent({
<Text <Text
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded8" borderRadius="$rounded8"
borderWidth={1} borderWidth="$spacing1"
color="$neutral1" color="$neutral1"
// fontFamily="SF Mono" // fontFamily="SF Mono"
px="$spacing8" px="$spacing8"
......
...@@ -33,7 +33,7 @@ export function LPRequestContent({ ...@@ -33,7 +33,7 @@ export function LPRequestContent({
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
flexDirection="row" flexDirection="row"
justifyContent="space-between" justifyContent="space-between"
p="$spacing16" p="$spacing16"
......
...@@ -66,7 +66,7 @@ export function PersonalSignRequestContent({ dappRequest }: PersonalSignRequestP ...@@ -66,7 +66,7 @@ export function PersonalSignRequestContent({ dappRequest }: PersonalSignRequestP
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
flexDirection="row" flexDirection="row"
justifyContent="space-between" justifyContent="space-between"
maxHeight={200} 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 ...@@ -34,7 +34,7 @@ export function Permit2RequestContent({ dappRequest }: Permit2RequestProps): JSX
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
flexDirection="column" flexDirection="column"
gap="$spacing12" gap="$spacing12"
p="$spacing16" p="$spacing16"
......
import { Component, ErrorInfo, ReactNode } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent'
import { UniswapXSwapRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent' import { UniswapXSwapRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent'
import { DomainContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/DomainContent' import { DomainContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/DomainContent'
import { MaybeExplorerLinkedAddress } from 'src/app/features/dappRequests/requestContent/SignTypeData/MaybeExplorerLinkedAddress' 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 { Permit2RequestContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent'
import { SignTypedDataRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' import { SignTypedDataRequest } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { EIP712Message, isEIP712TypedData } from 'src/app/features/dappRequests/types/EIP712Types' import { EIP712Message, isEIP712TypedData } from 'src/app/features/dappRequests/types/EIP712Types'
...@@ -11,42 +13,69 @@ import { Flex, Text } from 'ui/src' ...@@ -11,42 +13,69 @@ import { Flex, Text } from 'ui/src'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { isAddress } from 'utilities/src/addresses' import { isAddress } from 'utilities/src/addresses'
import { logger } from 'utilities/src/logger/logger'
interface SignTypedDataRequestProps { interface SignTypedDataRequestProps {
dappRequest: SignTypedDataRequest 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 { 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 { t } = useTranslation()
const parsedTypedData = JSON.parse(dappRequest.typedData) const parsedTypedData = JSON.parse(dappRequest.typedData)
if (!isEIP712TypedData(parsedTypedData)) { if (!isEIP712TypedData(parsedTypedData)) {
return ( return <NonStandardTypedDataRequestContent dappRequest={dappRequest} />
<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>
)
} }
const { name, version, chainId: domainChainId, verifyingContract, salt } = parsedTypedData?.domain || {} const { name, version, chainId: domainChainId, verifyingContract, salt } = parsedTypedData?.domain || {}
...@@ -116,7 +145,7 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques ...@@ -116,7 +145,7 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
flexDirection="column" flexDirection="column"
gap="$spacing4" gap="$spacing4"
maxHeight={200} maxHeight={200}
......
...@@ -193,7 +193,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf ...@@ -193,7 +193,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf
animation="quicker" animation="quicker"
borderColor="$surface2" borderColor="$surface2"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
disableRemoveScroll={false} disableRemoveScroll={false}
zIndex="$default" zIndex="$default"
{...animationPresets.fadeInDownOutUp} {...animationPresets.fadeInDownOutUp}
...@@ -259,7 +259,7 @@ function ConnectionStatusIcon({ ...@@ -259,7 +259,7 @@ function ConnectionStatusIcon({
<Circle <Circle
backgroundColor="$statusSuccess" backgroundColor="$statusSuccess"
borderColor="$surface1" borderColor="$surface1"
borderWidth={2} borderWidth="$spacing2"
height={iconSizes.icon12} height={iconSizes.icon12}
mr="$spacing8" mr="$spacing8"
position="absolute" position="absolute"
......
...@@ -72,7 +72,7 @@ export function SwitchNetworksModal(): JSX.Element { ...@@ -72,7 +72,7 @@ export function SwitchNetworksModal(): JSX.Element {
<Separator mb="$spacing4" mt="$spacing8" /> <Separator mb="$spacing4" mt="$spacing8" />
<Flex shrink overflow="scroll"> <Flex shrink $platform-web={{ overflow: 'auto' }}>
{enabledChains.map((chain: UniverseChainId) => { {enabledChains.map((chain: UniverseChainId) => {
return ( return (
<Popover.Close asChild> <Popover.Close asChild>
......
...@@ -24,7 +24,7 @@ export function PinReminder({ ...@@ -24,7 +24,7 @@ export function PinReminder({
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
gap="$spacing12" gap="$spacing12"
p="$spacing12" p="$spacing12"
shadowColor="$shadowColor" shadowColor="$shadowColor"
......
...@@ -198,7 +198,7 @@ export function ImportMnemonic(): JSX.Element { ...@@ -198,7 +198,7 @@ export function ImportMnemonic(): JSX.Element {
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius={100} borderRadius={100}
borderWidth={1} borderWidth="$spacing1"
mt="$spacing8" mt="$spacing8"
px="$spacing12" px="$spacing12"
py="$spacing8" py="$spacing8"
...@@ -307,7 +307,7 @@ const RecoveryPhraseWord = forwardRef< ...@@ -307,7 +307,7 @@ const RecoveryPhraseWord = forwardRef<
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
focusStyle={styles.inputFocus} focusStyle={styles.inputFocus}
fontSize={fonts.body3.fontSize} fontSize={fonts.body3.fontSize}
height={44} height={44}
......
...@@ -8,7 +8,7 @@ export function MainContentWrapper({ children }: PropsWithChildren): JSX.Element ...@@ -8,7 +8,7 @@ export function MainContentWrapper({ children }: PropsWithChildren): JSX.Element
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded32" borderRadius="$rounded32"
borderWidth={1} borderWidth="$spacing1"
pb="$spacing24" pb="$spacing24"
pt="$spacing48" pt="$spacing48"
px="$spacing24" px="$spacing24"
......
...@@ -17,7 +17,7 @@ export function MainIntroWrapper({ ...@@ -17,7 +17,7 @@ export function MainIntroWrapper({
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded32" borderRadius="$rounded32"
borderWidth={1} borderWidth="$spacing1"
overflow="hidden" overflow="hidden"
shadowColor="$shadowColor" shadowColor="$shadowColor"
shadowOpacity={0.1} shadowOpacity={0.1}
......
...@@ -197,7 +197,7 @@ export function OTPInput(): JSX.Element { ...@@ -197,7 +197,7 @@ export function OTPInput(): JSX.Element {
backgroundColor={character ? '$surface1' : '$surface2'} backgroundColor={character ? '$surface1' : '$surface2'}
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
disabled={loading} disabled={loading}
focusStyle={inputStyles.inputFocus} focusStyle={inputStyles.inputFocus}
fontSize={fonts.heading3.fontSize} fontSize={fonts.heading3.fontSize}
......
...@@ -14,12 +14,12 @@ import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen' ...@@ -14,12 +14,12 @@ import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingSteps' import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingSteps'
import { useScantasticContext } from 'src/app/features/onboarding/scan/ScantasticContextProvider' import { useScantasticContext } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
import { getScantasticUrl } from 'src/app/features/onboarding/scan/utils' 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 { navigate } from 'src/app/navigation/state'
import UAParser from 'ua-parser-js' 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 { 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 { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes, zIndices } from 'ui/src/theme' import { iconSizes, zIndices } from 'ui/src/theme'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
...@@ -82,7 +82,6 @@ export function ScanToOnboard(): JSX.Element { ...@@ -82,7 +82,6 @@ export function ScanToOnboard(): JSX.Element {
browser, browser,
model, model,
}) })
return getScantasticUrl(params) return getScantasticUrl(params)
} catch (e) { } catch (e) {
const wrappedError = new Error('Failed to build scantastic params', { cause: e }) const wrappedError = new Error('Failed to build scantastic params', { cause: e })
...@@ -189,6 +188,53 @@ export function ScanToOnboard(): JSX.Element { ...@@ -189,6 +188,53 @@ export function ScanToOnboard(): JSX.Element {
screen={ExtensionOnboardingScreens.OnboardingQRCode} screen={ExtensionOnboardingScreens.OnboardingQRCode}
> >
<OnboardingScreen <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={ Icon={
<Square <Square
backgroundColor="$surface2" backgroundColor="$surface2"
...@@ -210,11 +256,10 @@ export function ScanToOnboard(): JSX.Element { ...@@ -210,11 +256,10 @@ export function ScanToOnboard(): JSX.Element {
<Flex <Flex
alignContent="center" alignContent="center"
alignItems="center" alignItems="center"
backgroundColor={colors.white.val} backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth="$spacing1" borderWidth="$spacing1"
flexDirection="column"
my="$spacing24" my="$spacing24"
p="$spacing16" p="$spacing16"
position="relative" position="relative"
...@@ -223,8 +268,8 @@ export function ScanToOnboard(): JSX.Element { ...@@ -223,8 +268,8 @@ export function ScanToOnboard(): JSX.Element {
shadowRadius="$spacing4" shadowRadius="$spacing4"
> >
{errorDerivingQR ? ( {errorDerivingQR ? (
<Flex height={QR_CODE_SIZE} width={QR_CODE_SIZE}> <Flex px="$spacing16" height={QR_CODE_SIZE} width={QR_CODE_SIZE}>
<Text color="$statusCritical" m="auto" textAlign="center" variant="body2"> <Text color="$neutral2" m="auto" textAlign="center" variant="body3">
{t('onboarding.scan.error')} {t('onboarding.scan.error')}
</Text> </Text>
</Flex> </Flex>
......
...@@ -5961,7 +5961,7 @@ exports[`ReceiveScreen renders without error 1`] = ` ...@@ -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" 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 <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" data-testid="account-icon"
> >
<svg <svg
...@@ -11997,7 +11997,7 @@ exports[`ReceiveScreen renders without error 1`] = ` ...@@ -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" 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 <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" data-testid="account-icon"
> >
<svg <svg
......
...@@ -10,7 +10,7 @@ import { iconSizes } from 'ui/src/theme' ...@@ -10,7 +10,7 @@ import { iconSizes } from 'ui/src/theme'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' 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 { Account } from 'wallet/src/features/wallet/accounts/types'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
...@@ -45,7 +45,7 @@ export function RemoveRecoveryPhraseWallets(): JSX.Element { ...@@ -45,7 +45,7 @@ export function RemoveRecoveryPhraseWallets(): JSX.Element {
// TODO(@thomasthachil): merge this with mobile AccountList // TODO(@thomasthachil): merge this with mobile AccountList
function AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element { function AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element {
const addresses = useMemo(() => accounts.map((account) => account.address), [accounts]) const addresses = useMemo(() => accounts.map((account) => account.address), [accounts])
const { data, loading } = useAccountList({ const { data, loading } = useAccountListData({
addresses, addresses,
notifyOnNetworkStatusChange: true, notifyOnNetworkStatusChange: true,
}) })
...@@ -58,7 +58,7 @@ function AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Elem ...@@ -58,7 +58,7 @@ function AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Elem
.sort((a, b) => (b.balance ?? 0) - (a.balance ?? 0)) .sort((a, b) => (b.balance ?? 0) - (a.balance ?? 0))
return ( 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}> <ScrollView bounces={false}>
{sortedAddressesByBalance.map(({ address, balance }, index) => ( {sortedAddressesByBalance.map(({ address, balance }, index) => (
<AssociatedAccountRow <AssociatedAccountRow
......
...@@ -115,7 +115,7 @@ export function SeedPhraseDisplay({ mnemonicId }: { mnemonicId: string }): JSX.E ...@@ -115,7 +115,7 @@ export function SeedPhraseDisplay({ mnemonicId }: { mnemonicId: string }): JSX.E
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
gap="$spacing12" gap="$spacing12"
width="100%" width="100%"
> >
......
...@@ -61,7 +61,7 @@ export function UnitagIntroScreen(): JSX.Element { ...@@ -61,7 +61,7 @@ export function UnitagIntroScreen(): JSX.Element {
function UnitagIntroPill({ Icon, text }: { Icon: GeneratedIcon; text: string }): JSX.Element { function UnitagIntroPill({ Icon, text }: { Icon: GeneratedIcon; text: string }): JSX.Element {
return ( 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" /> <Icon color="$accent1" size="$icon.24" />
<Text color="$neutral2" variant="body1"> <Text color="$neutral2" variant="body1">
{text} {text}
......
...@@ -19,7 +19,7 @@ export const BaseEthereumRequestSchema = z.object({ ...@@ -19,7 +19,7 @@ export const BaseEthereumRequestSchema = z.object({
}) })
export const EthereumRequestWithIdSchema = BaseEthereumRequestSchema.extend({ export const EthereumRequestWithIdSchema = BaseEthereumRequestSchema.extend({
requestId: z.string(), requestId: z.string().uuid(),
}) })
export type EthereumRequestWithId = z.infer<typeof EthereumRequestWithIdSchema> export type EthereumRequestWithId = z.infer<typeof EthereumRequestWithIdSchema>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Uniswap Extension", "name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.15.0", "version": "1.16.0",
"minimum_chrome_version": "116", "minimum_chrome_version": "116",
"icons": { "icons": {
"16": "assets/icon16.png", "16": "assets/icon16.png",
......
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
v15Schema, v15Schema,
v16Schema, v16Schema,
v17Schema, v17Schema,
v18Schema,
v1Schema, v1Schema,
v2Schema, v2Schema,
v3Schema, v3Schema,
...@@ -43,6 +44,7 @@ import { ...@@ -43,6 +44,7 @@ import {
testActivatePendingAccounts, testActivatePendingAccounts,
testAddCreatedOnboardingRedesignAccount, testAddCreatedOnboardingRedesignAccount,
testAddedHapticSetting, testAddedHapticSetting,
testDeleteWelcomeWalletCard,
testMovedCurrencySetting, testMovedCurrencySetting,
testMovedLanguageSetting, testMovedLanguageSetting,
testMovedTokenWarnings, testMovedTokenWarnings,
...@@ -280,4 +282,8 @@ describe('Redux state migrations', () => { ...@@ -280,4 +282,8 @@ describe('Redux state migrations', () => {
it('migrates from v17 to v18', () => { it('migrates from v17 to v18', () => {
testUnchecksumDismissedTokenWarningKeys(migrations[18], v17Schema) testUnchecksumDismissedTokenWarningKeys(migrations[18], v17Schema)
}) })
it('migrates from v18 to v19', () => {
testDeleteWelcomeWalletCard(migrations[19], v18Schema)
})
}) })
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
deleteDefaultFavoritesFromFavoritesState, deleteDefaultFavoritesFromFavoritesState,
deleteExtensionOnboardingState, deleteExtensionOnboardingState,
deleteHoldToSwapBehaviorHistory, deleteHoldToSwapBehaviorHistory,
deleteWelcomeWalletCardBehaviorHistory,
moveCurrencySetting, moveCurrencySetting,
moveDismissedTokenWarnings, moveDismissedTokenWarnings,
moveLanguageSetting, moveLanguageSetting,
...@@ -44,6 +45,7 @@ export const migrations = { ...@@ -44,6 +45,7 @@ export const migrations = {
16: updateExploreOrderByType, 16: updateExploreOrderByType,
17: removeCreatedOnboardingRedesignAccountBehaviorHistory, 17: removeCreatedOnboardingRedesignAccountBehaviorHistory,
18: unchecksumDismissedTokenWarningKeys, 18: unchecksumDismissedTokenWarningKeys,
19: deleteWelcomeWalletCardBehaviorHistory,
} }
export const EXTENSION_STATE_VERSION = 18 export const EXTENSION_STATE_VERSION = 19
...@@ -204,4 +204,14 @@ const v17SchemaIntermediate = { ...@@ -204,4 +204,14 @@ const v17SchemaIntermediate = {
delete v17SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount delete v17SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount
export const v17Schema = v17SchemaIntermediate 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/* ...@@ -114,6 +114,7 @@ ios/WidgetsCore/MobileSchema/*
# Swift env # Swift env
ios/WidgetsCore/Env.swift ios/WidgetsCore/Env.swift
ios/OneSignalNotificationServiceExtension/Env.swift
# Sentry # Sentry
ios/sentry.properties ios/sentry.properties
......
...@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isE2E) { ...@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isE2E) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
} }
def devVersionName = "1.45" def devVersionName = "1.46"
def betaVersionName = "1.45" def betaVersionName = "1.46"
def prodVersionName = "1.45" def prodVersionName = "1.46"
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
......
...@@ -77,7 +77,7 @@ class NotificationExtension : OSRemoteNotificationReceivedHandler { ...@@ -77,7 +77,7 @@ class NotificationExtension : OSRemoteNotificationReceivedHandler {
private const val STATSIG_ENVIRONMENT_KEY_TIER = "tier" private const val STATSIG_ENVIRONMENT_KEY_TIER = "tier"
private const val FEATURE_GATE_UNFUNDED_WALLET = "notification_unfunded_wallet" 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 FIELD_NOTIFICATION_TYPE = "notification_type"
private const val TYPE_UNFUNDED_WALLET_REMINDER = "unfunded_wallet_reminder" private const val TYPE_UNFUNDED_WALLET_REMINDER = "unfunded_wallet_reminder"
......
...@@ -11,8 +11,6 @@ ...@@ -11,8 +11,6 @@
</dict> </dict>
<key>OneSignal_app_groups_key</key> <key>OneSignal_app_groups_key</key>
<string>group.com.uniswap.mobile.onesignal</string> <string>group.com.uniswap.mobile.onesignal</string>
<key>STATSIG_SDK_KEY</key>
<string>$(STATSIG_SDK_KEY)</string>
<key>BUNDLE_ID_SUFFIX</key> <key>BUNDLE_ID_SUFFIX</key>
<string>$(BUNDLE_ID_SUFFIX)</string> <string>$(BUNDLE_ID_SUFFIX)</string>
</dict> </dict>
......
...@@ -43,7 +43,7 @@ class NotificationService: UNNotificationServiceExtension { ...@@ -43,7 +43,7 @@ class NotificationService: UNNotificationServiceExtension {
if (!Statsig.isInitialized()) { if (!Statsig.isInitialized()) {
// The real sdk key is needed on iOS even though it's substituted in proxy // 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 // 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( let statsigUser = StatsigUser(
userID: UIDevice.current.identifierForVendor?.uuidString, userID: UIDevice.current.identifierForVendor?.uuidString,
custom: [ custom: [
...@@ -88,5 +88,5 @@ struct Constants { ...@@ -88,5 +88,5 @@ struct Constants {
static let typePriceAlert = "price_alert" static let typePriceAlert = "price_alert"
static let gateUnfundedWallet = "notification_unfunded_wallet" static let gateUnfundedWallet = "notification_unfunded_wallet"
static let gatePriceAlert = "notification_price_alert" static let gatePriceAlert = "notification_price_alerts"
} }
...@@ -1204,7 +1204,7 @@ PODS: ...@@ -1204,7 +1204,7 @@ PODS:
- ExpoModulesCore - ExpoModulesCore
- ExpoBlur (12.9.2): - ExpoBlur (12.9.2):
- ExpoModulesCore - ExpoModulesCore
- ExpoCamera (14.1.2): - ExpoCamera (14.1.1):
- ExpoModulesCore - ExpoModulesCore
- ZXingObjC/OneD - ZXingObjC/OneD
- ZXingObjC/PDF417 - ZXingObjC/PDF417
...@@ -3014,7 +3014,7 @@ SPEC CHECKSUMS: ...@@ -3014,7 +3014,7 @@ SPEC CHECKSUMS:
EXImageLoader: 55080616b2fe9da19ef8c7f706afd9814e279b6b EXImageLoader: 55080616b2fe9da19ef8c7f706afd9814e279b6b
Expo: 0f04015e4254c8b7c74f3c4b2facdfd0b62152b0 Expo: 0f04015e4254c8b7c74f3c4b2facdfd0b62152b0
ExpoBlur: e832d874bd94afc0645daddbd3162ec1ce172080 ExpoBlur: e832d874bd94afc0645daddbd3162ec1ce172080
ExpoCamera: ed3dd4ae8e32603c91336ea8e27c5d1e2822d8f6 ExpoCamera: 53f5ef81bab088b2edcf94d05e4da3bc84f46383
ExpoClipboard: b597982124f067ff9f5b89093eb3d97898d5d877 ExpoClipboard: b597982124f067ff9f5b89093eb3d97898d5d877
ExpoFileSystem: eecaf6796aed0f4dd20042dc2ca2cac6c4bc1185 ExpoFileSystem: eecaf6796aed0f4dd20042dc2ca2cac6c4bc1185
ExpoHaptics: 28a771b630353cd6e8dcf1b1e3e693e38ad7c3c3 ExpoHaptics: 28a771b630353cd6e8dcf1b1e3e693e38ad7c3c3
......
...@@ -28,6 +28,7 @@ jest.mock('react-native-onesignal', () => { ...@@ -28,6 +28,7 @@ jest.mock('react-native-onesignal', () => {
promptForPushNotificationsWithUserResponse: jest.fn(), promptForPushNotificationsWithUserResponse: jest.fn(),
setNotificationWillShowInForegroundHandler: jest.fn(), setNotificationWillShowInForegroundHandler: jest.fn(),
setNotificationOpenedHandler: jest.fn(), setNotificationOpenedHandler: jest.fn(),
sendTag: jest.fn(),
getDeviceState: () => ({ userId: 'dummyUserId', pushToken: 'dummyPushToken' }), getDeviceState: () => ({ userId: 'dummyUserId', pushToken: 'dummyPushToken' }),
} }
}) })
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"android:prod": "react-native run-android --mode=prodDebug", "android:prod": "react-native run-android --mode=prodDebug",
"android:prod:release": "react-native run-android --mode=prodRelease", "android:prod:release": "react-native run-android --mode=prodRelease",
"check:deps:usage": "./scripts/checkDepsUsage.sh", "check:deps:usage": "./scripts/checkDepsUsage.sh",
"check:bundlesize": "./scripts/checkBundleSize.sh",
"clean": "react-native-clean-project", "clean": "react-native-clean-project",
"debug": "react-devtools", "debug": "react-devtools",
"debug:reactotron:install": "./scripts/installDebugger.sh", "debug:reactotron:install": "./scripts/installDebugger.sh",
...@@ -26,13 +27,13 @@ ...@@ -26,13 +27,13 @@
"firestore:deploy:rules": "firebase deploy --only firestore:rules", "firestore:deploy:rules": "firebase deploy --only firestore:rules",
"link:assets": "react-native-asset", "link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate", "graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 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": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios",
"ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift", "ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift",
"ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"", "ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"",
"ios:dev:release": "react-native run-ios --configuration Dev", "ios:dev:release": "react-native run-ios --configuration Dev",
"ios:beta": "react-native run-ios --configuration Beta", "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", "ios:release": "react-native run-ios --configuration Release",
"format": "../../scripts/prettier.sh", "format": "../../scripts/prettier.sh",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0",
...@@ -99,7 +100,7 @@ ...@@ -99,7 +100,7 @@
"expo": "50.0.15", "expo": "50.0.15",
"expo-barcode-scanner": "12.9.3", "expo-barcode-scanner": "12.9.3",
"expo-blur": "12.9.2", "expo-blur": "12.9.2",
"expo-camera": "14.1.2", "expo-camera": "14.1.1",
"expo-clipboard": "5.0.1", "expo-clipboard": "5.0.1",
"expo-haptics": "12.8.1", "expo-haptics": "12.8.1",
"expo-linear-gradient": "12.7.2", "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 ...@@ -2,8 +2,8 @@ import os
ENV_DEFAULTS_FILE = '../../.env.defaults' ENV_DEFAULTS_FILE = '../../.env.defaults'
ENV_DEFAULTS_LOCAL_FILE = '../../.env.defaults.local' ENV_DEFAULTS_LOCAL_FILE = '../../.env.defaults.local'
SWIFT_FILE_PATH = 'ios/WidgetsCore/Env.swift' SWIFT_FILE_PATHS = ['ios/WidgetsCore/Env.swift', 'ios/OneSignalNotificationServiceExtension/Env.swift']
SWIFT_ENV_VARIABLES = ['UNISWAP_API_KEY'] SWIFT_ENV_VARIABLES = ['UNISWAP_API_KEY', 'STATSIG_API_KEY']
def to_swift_constant_line(key, value): def to_swift_constant_line(key, value):
return f' static let {key.upper()} = "{value}"' return f' static let {key.upper()} = "{value}"'
...@@ -22,7 +22,7 @@ def process_lines(lines, search_vars): ...@@ -22,7 +22,7 @@ def process_lines(lines, search_vars):
return env_var_declarations return env_var_declarations
# convert env variables to swift constants and writes to a swift file. # 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() envs_left_to_find = env_variables.copy()
env_var_declarations = [] env_var_declarations = []
...@@ -44,6 +44,7 @@ def copy_env_vars_to_swift(env_defaults_file, env_defaults_local_file, swift_fil ...@@ -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)) env_var_declarations.extend(process_lines(default_env_lines, envs_left_to_find))
# write to swift file # write to swift file
for swift_file in swift_files:
with open(swift_file, 'w') as f: with open(swift_file, 'w') as f:
f.write('struct Env {\n') f.write('struct Env {\n')
f.write('\n'.join(env_var_declarations)) 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 ...@@ -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.') print('WARNING: Not all environment variables were converted to Swift.')
exit(1) 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' ...@@ -11,6 +11,7 @@ import appsFlyer from 'react-native-appsflyer'
import DeviceInfo from 'react-native-device-info' import DeviceInfo from 'react-native-device-info'
import { GestureHandlerRootView } from 'react-native-gesture-handler' import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { MMKV } from 'react-native-mmkv' import { MMKV } from 'react-native-mmkv'
import OneSignal from 'react-native-onesignal'
import { SafeAreaProvider } from 'react-native-safe-area-context' import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens' import { enableFreeze } from 'react-native-screens'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
...@@ -29,8 +30,9 @@ import { LockScreenContextProvider } from 'src/features/authentication/lockScree ...@@ -29,8 +30,9 @@ import { LockScreenContextProvider } from 'src/features/authentication/lockScree
import { BiometricContextProvider } from 'src/features/biometrics/context' import { BiometricContextProvider } from 'src/features/biometrics/context'
import { NotificationToastWrapper } from 'src/features/notifications/NotificationToastWrapper' import { NotificationToastWrapper } from 'src/features/notifications/NotificationToastWrapper'
import { initOneSignal } from 'src/features/notifications/Onesignal' import { initOneSignal } from 'src/features/notifications/Onesignal'
import { AIAssistantScreen } from 'src/features/openai/AIAssistantScreen' import { OneSignalUserTagField } from 'src/features/notifications/constants'
import { OpenAIContextProvider } from 'src/features/openai/OpenAIContext' import { DevAIAssistantScreen } from 'src/features/openai/DevAIAssistantScreen'
import { DevOpenAIProvider } from 'src/features/openai/DevOpenAIProvider'
import { shouldLogScreen } from 'src/features/telemetry/directLogScreens' import { shouldLogScreen } from 'src/features/telemetry/directLogScreens'
import { selectCustomEndpoint } from 'src/features/tweaks/selectors' import { selectCustomEndpoint } from 'src/features/tweaks/selectors'
import { import {
...@@ -55,7 +57,7 @@ import { ...@@ -55,7 +57,7 @@ import {
import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants'
import { Experiments } from 'uniswap/src/features/gating/experiments' import { Experiments } from 'uniswap/src/features/gating/experiments'
import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' 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 { loadStatsigOverrides } from 'uniswap/src/features/gating/overrides/customPersistedOverrides'
import { Statsig, StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig' import { Statsig, StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
...@@ -74,6 +76,7 @@ import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utiliti ...@@ -74,6 +76,7 @@ import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utiliti
import { registerConsoleOverrides } from 'utilities/src/logger/console' import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { DDRumAction, DDRumTiming } from 'utilities/src/logger/datadogEvents' import { DDRumAction, DDRumTiming } from 'utilities/src/logger/datadogEvents'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { isIOS } from 'utilities/src/platform'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext' import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
...@@ -250,6 +253,17 @@ function AppOuter(): JSX.Element | null { ...@@ -250,6 +253,17 @@ function AppOuter(): JSX.Element | null {
Statsig.getExperimentWithExposureLoggingDisabled(experiment).getGroupName(), Statsig.getExperimentWithExposureLoggingDisabled(experiment).getGroupName(),
).catch(() => undefined) ).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) { if (!client) {
...@@ -270,7 +284,7 @@ function AppOuter(): JSX.Element | null { ...@@ -270,7 +284,7 @@ function AppOuter(): JSX.Element | null {
<DataUpdaters /> <DataUpdaters />
<NavigationContainer> <NavigationContainer>
<MobileWalletNavigationProvider> <MobileWalletNavigationProvider>
<OpenAIContextProvider> <DevOpenAIProvider>
<WalletUniswapProvider> <WalletUniswapProvider>
<BottomSheetModalProvider> <BottomSheetModalProvider>
<AppModals /> <AppModals />
...@@ -280,7 +294,7 @@ function AppOuter(): JSX.Element | null { ...@@ -280,7 +294,7 @@ function AppOuter(): JSX.Element | null {
</BottomSheetModalProvider> </BottomSheetModalProvider>
</WalletUniswapProvider> </WalletUniswapProvider>
<NotificationToastWrapper /> <NotificationToastWrapper />
</OpenAIContextProvider> </DevOpenAIProvider>
</MobileWalletNavigationProvider> </MobileWalletNavigationProvider>
</NavigationContainer> </NavigationContainer>
</LockScreenContextProvider> </LockScreenContextProvider>
...@@ -331,11 +345,9 @@ function AppInner(): JSX.Element { ...@@ -331,11 +345,9 @@ function AppInner(): JSX.Element {
NativeModules.ThemeModule.setColorScheme(themeSetting) NativeModules.ThemeModule.setColorScheme(themeSetting)
}, [themeSetting]) }, [themeSetting])
const openAIAssistantEnabled = useFeatureFlag(FeatureFlags.OpenAIAssistant)
return ( return (
<> <>
{openAIAssistantEnabled && <AIAssistantScreen />} <DevAIAssistantScreen />
<OfflineBanner /> <OfflineBanner />
<TestnetModeBanner /> <TestnetModeBanner />
<AppStackNavigator /> <AppStackNavigator />
......
...@@ -84,6 +84,7 @@ import { ...@@ -84,6 +84,7 @@ import {
v7Schema, v7Schema,
v80Schema, v80Schema,
v81Schema, v81Schema,
v83Schema,
v8Schema, v8Schema,
v9Schema, v9Schema,
} from 'src/app/schema' } from 'src/app/schema'
...@@ -92,6 +93,7 @@ import { initialBiometricsSettingsState } from 'src/features/biometrics/slice' ...@@ -92,6 +93,7 @@ import { initialBiometricsSettingsState } from 'src/features/biometrics/slice'
import { initialCloudBackupState } from 'src/features/CloudBackup/cloudBackupSlice' import { initialCloudBackupState } from 'src/features/CloudBackup/cloudBackupSlice'
import { initialPasswordLockoutState } from 'src/features/CloudBackup/passwordLockoutSlice' import { initialPasswordLockoutState } from 'src/features/CloudBackup/passwordLockoutSlice'
import { initialModalsState } from 'src/features/modals/modalSlice' import { initialModalsState } from 'src/features/modals/modalSlice'
import { initialPushNotificationsState } from 'src/features/notifications/slice'
import { initialTweaksState } from 'src/features/tweaks/slice' import { initialTweaksState } from 'src/features/tweaks/slice'
import { initialWalletConnectState } from 'src/features/walletConnect/walletConnectSlice' import { initialWalletConnectState } from 'src/features/walletConnect/walletConnectSlice'
import { AccountType } from 'uniswap/src/features/accounts/types' import { AccountType } from 'uniswap/src/features/accounts/types'
...@@ -120,6 +122,7 @@ import { ...@@ -120,6 +122,7 @@ import {
testActivatePendingAccounts, testActivatePendingAccounts,
testAddCreatedOnboardingRedesignAccount, testAddCreatedOnboardingRedesignAccount,
testAddedHapticSetting, testAddedHapticSetting,
testDeleteWelcomeWalletCard,
testMovedCurrencySetting, testMovedCurrencySetting,
testMovedLanguageSetting, testMovedLanguageSetting,
testMovedTokenWarnings, testMovedTokenWarnings,
...@@ -194,6 +197,7 @@ describe('Redux state migrations', () => { ...@@ -194,6 +197,7 @@ describe('Redux state migrations', () => {
passwordLockout: initialPasswordLockoutState, passwordLockout: initialPasswordLockoutState,
behaviorHistory: initialBehaviorHistoryState, behaviorHistory: initialBehaviorHistoryState,
providers: { isInitialized: false }, providers: { isInitialized: false },
pushNotifications: initialPushNotificationsState,
saga: {}, saga: {},
searchHistory: initialSearchHistoryState, searchHistory: initialSearchHistoryState,
telemetry: initialTelemetryState, telemetry: initialTelemetryState,
...@@ -1598,4 +1602,17 @@ describe('Redux state migrations', () => { ...@@ -1598,4 +1602,17 @@ describe('Redux state migrations', () => {
it('migrates from v81 to v82', () => { it('migrates from v81 to v82', () => {
testUnchecksumDismissedTokenWarningKeys(migrations[82], v81Schema) 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 { ...@@ -30,6 +30,7 @@ import {
deleteDefaultFavoritesFromFavoritesState, deleteDefaultFavoritesFromFavoritesState,
deleteExtensionOnboardingState, deleteExtensionOnboardingState,
deleteHoldToSwapBehaviorHistory, deleteHoldToSwapBehaviorHistory,
deleteWelcomeWalletCardBehaviorHistory,
moveCurrencySetting, moveCurrencySetting,
moveDismissedTokenWarnings, moveDismissedTokenWarnings,
moveLanguageSetting, moveLanguageSetting,
...@@ -957,6 +958,27 @@ export const migrations = { ...@@ -957,6 +958,27 @@ export const migrations = {
81: removeCreatedOnboardingRedesignAccountBehaviorHistory, 81: removeCreatedOnboardingRedesignAccountBehaviorHistory,
82: unchecksumDismissedTokenWarningKeys, 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 { combineReducers } from '@reduxjs/toolkit'
import { monitoredSagaReducers } from 'src/app/saga' import { monitoredSagaReducers } from 'src/app/monitoredSagas'
import { cloudBackupReducer } from 'src/features/CloudBackup/cloudBackupSlice' import { cloudBackupReducer } from 'src/features/CloudBackup/cloudBackupSlice'
import { passwordLockoutReducer } from 'src/features/CloudBackup/passwordLockoutSlice' import { passwordLockoutReducer } from 'src/features/CloudBackup/passwordLockoutSlice'
import { biometricSettingsReducer } from 'src/features/biometrics/slice' import { biometricSettingsReducer } from 'src/features/biometrics/slice'
import { modalsReducer } from 'src/features/modals/modalSlice' import { modalsReducer } from 'src/features/modals/modalSlice'
import { pushNotificationsReducer } from 'src/features/notifications/slice'
import { tweaksReducer } from 'src/features/tweaks/slice' import { tweaksReducer } from 'src/features/tweaks/slice'
import { walletConnectReducer } from 'src/features/walletConnect/walletConnectSlice' import { walletConnectReducer } from 'src/features/walletConnect/walletConnectSlice'
import { walletPersistedStateList, walletReducers } from 'wallet/src/state/walletReducer' import { walletPersistedStateList, walletReducers } from 'wallet/src/state/walletReducer'
...@@ -14,6 +15,7 @@ const mobileReducers = { ...@@ -14,6 +15,7 @@ const mobileReducers = {
cloudBackup: cloudBackupReducer, cloudBackup: cloudBackupReducer,
modals: modalsReducer, modals: modalsReducer,
passwordLockout: passwordLockoutReducer, passwordLockout: passwordLockoutReducer,
pushNotifications: pushNotificationsReducer,
saga: monitoredSagaReducers, saga: monitoredSagaReducers,
tweaks: tweaksReducer, tweaks: tweaksReducer,
walletConnect: walletConnectReducer, walletConnect: walletConnectReducer,
...@@ -27,6 +29,7 @@ export const mobilePersistedStateList: Array<keyof typeof mobileReducers> = [ ...@@ -27,6 +29,7 @@ export const mobilePersistedStateList: Array<keyof typeof mobileReducers> = [
'passwordLockout', 'passwordLockout',
'tweaks', 'tweaks',
'cloudBackup', 'cloudBackup',
'pushNotifications',
] ]
export type MobileState = ReturnType<typeof mobileReducer> export type MobileState = ReturnType<typeof mobileReducer>
...@@ -70,6 +70,7 @@ export function NotificationsOSSettingsModal({ navigation }: NotificationsOSSett ...@@ -70,6 +70,7 @@ export function NotificationsOSSettingsModal({ navigation }: NotificationsOSSett
<Flex animation="fast" gap="$spacing40" pb="$spacing12" px="$spacing24" width="100%"> <Flex animation="fast" gap="$spacing40" pb="$spacing12" px="$spacing24" width="100%">
<GenericHeader <GenericHeader
Icon={BellOn} Icon={BellOn}
flexProps={{ m: '$spacing12' }}
subtitle={t('onboarding.notification.subtitle')} subtitle={t('onboarding.notification.subtitle')}
title={t('onboarding.notification.title')} 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 { ...@@ -441,6 +441,9 @@ export function AppStackNavigator(): JSX.Element {
<AppStack.Group screenOptions={navNativeStackOptions.presentationModal}> <AppStack.Group screenOptions={navNativeStackOptions.presentationModal}>
<AppStack.Screen component={EducationScreen} name={MobileScreens.Education} /> <AppStack.Screen component={EducationScreen} name={MobileScreens.Education} />
</AppStack.Group> </AppStack.Group>
<AppStack.Group screenOptions={navNativeStackOptions.presentationBottomSheet}>
<AppStack.Screen component={NotificationsOSSettingsModal} name={ModalName.NotificationsOSSettings} />
</AppStack.Group>
{isDevEnv() && <AppStack.Screen component={StorybookUIRoot} name={MobileScreens.Storybook} />} {isDevEnv() && <AppStack.Screen component={StorybookUIRoot} name={MobileScreens.Storybook} />}
</AppStack.Navigator> </AppStack.Navigator>
) )
......
...@@ -124,6 +124,7 @@ export type AppStackParamList = { ...@@ -124,6 +124,7 @@ export type AppStackParamList = {
} }
[MobileScreens.WebView]: { headerTitle: string; uriLink: string } [MobileScreens.WebView]: { headerTitle: string; uriLink: string }
[MobileScreens.Storybook]: undefined [MobileScreens.Storybook]: undefined
[ModalName.NotificationsOSSettings]: undefined
} }
export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList> export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList>
......
import { PersistState } from 'redux-persist' import { PersistState } from 'redux-persist'
import { monitoredSagas } from 'src/app/monitoredSagas'
import { cloudBackupsManagerSaga } from 'src/features/CloudBackup/saga' import { cloudBackupsManagerSaga } from 'src/features/CloudBackup/saga'
import { appRatingWatcherSaga } from 'src/features/appRating/saga' import { appRatingWatcherSaga } from 'src/features/appRating/saga'
import { deepLinkWatcher } from 'src/features/deepLinking/handleDeepLinkSaga' import { deepLinkWatcher } from 'src/features/deepLinking/handleDeepLinkSaga'
import { firebaseDataWatcher } from 'src/features/firebase/firebaseDataSaga' import { firebaseDataWatcher } from 'src/features/firebase/firebaseDataSaga'
import { modalWatcher } from 'src/features/modals/saga' import { modalWatcher } from 'src/features/modals/saga'
import { pushNotificationsWatcherSaga } from 'src/features/notifications/saga'
import { telemetrySaga } from 'src/features/telemetry/saga' import { telemetrySaga } from 'src/features/telemetry/saga'
import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga' import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga'
import { walletConnectSaga } from 'src/features/walletConnect/saga' import { walletConnectSaga } from 'src/features/walletConnect/saga'
...@@ -11,27 +13,7 @@ import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga' ...@@ -11,27 +13,7 @@ import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga'
import { call, delay, select, spawn } from 'typed-redux-saga' import { call, delay, select, spawn } from 'typed-redux-saga'
import { appLanguageWatcherSaga } from 'uniswap/src/features/language/saga' import { appLanguageWatcherSaga } from 'uniswap/src/features/language/saga'
import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient' 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 { 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 const REHYDRATION_STATUS_POLLING_INTERVAL = 50
...@@ -43,42 +25,13 @@ const sagas = [ ...@@ -43,42 +25,13 @@ const sagas = [
deepLinkWatcher, deepLinkWatcher,
firebaseDataWatcher, firebaseDataWatcher,
modalWatcher, modalWatcher,
pushNotificationsWatcherSaga,
restoreMnemonicCompleteWatcher, restoreMnemonicCompleteWatcher,
signWcRequestSaga, signWcRequestSaga,
telemetrySaga, telemetrySaga,
walletConnectSaga, 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() { export function* rootMobileSaga() {
// wait until redux-persist has finished rehydration // wait until redux-persist has finished rehydration
while (true) { while (true) {
......
...@@ -637,6 +637,25 @@ const v81SchemaIntermediate = { ...@@ -637,6 +637,25 @@ const v81SchemaIntermediate = {
delete v81SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount delete v81SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount
export const v81Schema = v81SchemaIntermediate 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 // TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer
// export const getSchema = (): RootState => v0Schema // export const getSchema = (): RootState => v0Schema
export const getSchema = (): typeof v81Schema => v81Schema export const getSchema = (): typeof v84Schema => v84Schema
import { BarCodeScanner } from 'expo-barcode-scanner' import { PermissionStatus, scanFromURLAsync } from 'expo-barcode-scanner'
import { AutoFocus, BarCodeScanningResult, Camera, CameraType } from 'expo-camera' import { BarCodeScanningResult, CameraType } from 'expo-camera'
import { PermissionStatus } from 'expo-modules-core' import { CameraProps, CameraView, useCameraPermissions } from 'expo-camera/next'
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, LayoutChangeEvent, LayoutRectangle, StyleSheet } from 'react-native' import { Alert, LayoutChangeEvent, LayoutRectangle, StyleSheet } from 'react-native'
import DeviceInfo from 'react-native-device-info'
import { launchImageLibrary } from 'react-native-image-picker' import { launchImageLibrary } from 'react-native-image-picker'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg' 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 CameraScan from 'ui/src/assets/icons/camera-scan.svg'
import { Global, PhotoStacked } from 'ui/src/components/icons' import { Global, PhotoStacked } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
...@@ -16,9 +16,13 @@ import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' ...@@ -16,9 +16,13 @@ import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { useSporeColorsForTheme } from 'ui/src/hooks/useSporeColors' import { useSporeColorsForTheme } from 'ui/src/hooks/useSporeColors'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import PasteButton from 'uniswap/src/components/buttons/PasteButton' 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' import { openSettings } from 'wallet/src/utils/linking'
enum BarcodeType {
QR = 'qr',
}
type QRCodeScannerProps = { type QRCodeScannerProps = {
onScanCode: (data: string) => void onScanCode: (data: string) => void
shouldFreezeCamera: boolean shouldFreezeCamera: boolean
...@@ -48,17 +52,14 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -48,17 +52,14 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
const dimensions = useDeviceDimensions() const dimensions = useDeviceDimensions()
const [permissionResponse, requestPermissionResponse] = Camera.useCameraPermissions() const [permission, requestPermission] = useCameraPermissions()
const permissionStatus = permissionResponse?.status
const [autoFocus, setAutoFocus] = useState(AutoFocus.off)
const [isReadingImageFile, setIsReadingImageFile] = useState(false) const [isReadingImageFile, setIsReadingImageFile] = useState(false)
const [overlayLayout, setOverlayLayout] = useState<LayoutRectangle | null>() const [overlayLayout, setOverlayLayout] = useState<LayoutRectangle | null>()
const [infoLayout, setInfoLayout] = useState<LayoutRectangle | null>() const [infoLayout, setInfoLayout] = useState<LayoutRectangle | null>()
const [bottomLayout, setBottomLayout] = useState<LayoutRectangle | null>() const [bottomLayout, setBottomLayout] = useState<LayoutRectangle | null>()
const handleBarCodeScanned = useCallback( const handleBarcodeScanned = useCallback(
(result: BarCodeScanningResult): void => { (result: BarCodeScanningResult): void => {
if (shouldFreezeCamera) { if (shouldFreezeCamera) {
return return
...@@ -89,7 +90,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -89,7 +90,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
return return
} }
const result = (await BarCodeScanner.scanFromURLAsync(uri, [BarCodeScanner.Constants.BarCodeType.qr]))[0] const result = (await scanFromURLAsync(uri, [BarcodeType.QR]))[0]
if (!result) { if (!result) {
Alert.alert(t('qrScanner.error.none')) Alert.alert(t('qrScanner.error.none'))
...@@ -97,15 +98,16 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -97,15 +98,16 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
return return
} }
handleBarCodeScanned(result) handleBarcodeScanned(result)
}, [handleBarCodeScanned, isReadingImageFile, t]) }, [handleBarcodeScanned, isReadingImageFile, t])
useEffect(() => { useEffect(() => {
const handlePermissionStatus = async (): Promise<void> => { const handlePermissionStatus = async (): Promise<void> => {
const cameraState = await requestPermissionResponse() if (permission?.granted) {
const latestPermissionStatus = cameraState.status return
}
if ([PermissionStatus.UNDETERMINED, PermissionStatus.DENIED].includes(latestPermissionStatus)) { const { status } = await requestPermission()
if ([PermissionStatus.UNDETERMINED, PermissionStatus.DENIED].includes(status)) {
Alert.alert(t('qrScanner.error.camera.title'), t('qrScanner.error.camera.message'), [ Alert.alert(t('qrScanner.error.camera.title'), t('qrScanner.error.camera.message'), [
{ text: t('common.navigation.systemSettings'), onPress: openSettings }, { text: t('common.navigation.systemSettings'), onPress: openSettings },
{ {
...@@ -115,46 +117,36 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -115,46 +117,36 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
} }
} }
handlePermissionStatus().catch(() => {}) handlePermissionStatus().catch((error) => {
}, [requestPermissionResponse, t]) logger.error(error, {
tags: { file: 'QRCodeScanner.tsx', function: 'handlePermissionStatus' },
})
})
}, [permission?.granted, t, requestPermission])
const overlayWidth = (overlayLayout?.height ?? 0) / CAMERA_ASPECT_RATIO const overlayWidth = (overlayLayout?.height ?? 0) / CAMERA_ASPECT_RATIO
const cameraWidth = dimensions.fullWidth const cameraWidth = dimensions.fullWidth
const cameraHeight = CAMERA_ASPECT_RATIO * cameraWidth const cameraHeight = CAMERA_ASPECT_RATIO * cameraWidth
const scannerSize = Math.min(overlayWidth, cameraWidth) * SCAN_ICON_WIDTH_RATIO const scannerSize = Math.min(overlayWidth, cameraWidth) * SCAN_ICON_WIDTH_RATIO
/** const disableMicPrompt: CameraProps = {
* Resets the camera auto focus to force the camera to refocus by toggling mute: true,
* the auto focus off and on. This allows us to manually let the user refocus mode: 'picture',
* 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()
} }
return ( return (
<AnimatedFlex grow theme={theme} borderRadius="$rounded12" entering={FadeIn} exiting={FadeOut} overflow="hidden"> <AnimatedFlex grow theme={theme} borderRadius="$rounded12" entering={FadeIn} exiting={FadeOut} overflow="hidden">
<Flex justifyContent="center" style={StyleSheet.absoluteFill}> <Flex justifyContent="center" style={StyleSheet.absoluteFill}>
<Flex height={cameraHeight} overflow="hidden" width={cameraWidth}> <Flex height={cameraHeight} overflow="hidden" width={cameraWidth}>
{permissionStatus === PermissionStatus.GRANTED && !isReadingImageFile && ( {permission?.granted && !isReadingImageFile && (
<Camera <CameraView
barCodeScannerSettings={{ {...disableMicPrompt}
barCodeTypes: [BarCodeScanner.Constants.BarCodeType.qr], barcodeScannerSettings={{
barcodeTypes: [BarcodeType.QR],
}} }}
facing={CameraType.back}
style={StyleSheet.absoluteFillObject} style={StyleSheet.absoluteFillObject}
type={CameraType.back} onBarcodeScanned={handleBarcodeScanned}
autoFocus={autoFocus}
onBarCodeScanned={handleBarCodeScanned}
/> />
)} )}
</Flex> </Flex>
...@@ -189,10 +181,8 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -189,10 +181,8 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
</Text> </Text>
</Flex> </Flex>
{!shouldFreezeCamera ? ( {!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} /> <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..." // camera has been frozen (has seen a barcode) — show the loading spinner and "Connecting..." or "Loading..."
<Flex height={scannerSize} width={scannerSize}> <Flex height={scannerSize} width={scannerSize}>
...@@ -211,9 +201,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -211,9 +201,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
</Flex> </Flex>
</Flex> </Flex>
)} )}
<DevelopmentOnly> {DeviceInfo.isEmulatorSync() && !shouldFreezeCamera && (
{/* when in development mode AND there's no camera (using iOS Simulator), add a paste button */}
{!shouldFreezeCamera ? (
<Flex centered height={scannerSize} style={[StyleSheet.absoluteFill]} width={scannerSize}> <Flex centered height={scannerSize} style={[StyleSheet.absoluteFill]} width={scannerSize}>
<Flex <Flex
backgroundColor="$surface2" backgroundColor="$surface2"
...@@ -229,9 +217,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -229,9 +217,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
<PasteButton onPress={onScanCode} /> <PasteButton onPress={onScanCode} />
</Flex> </Flex>
</Flex> </Flex>
) : null} )}
</DevelopmentOnly>
<Flex <Flex
alignItems="center" alignItems="center"
bottom={0} bottom={0}
......
...@@ -85,7 +85,7 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E ...@@ -85,7 +85,7 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E
<TouchableArea <TouchableArea
borderColor={isDarkMode ? '$transparent' : '$surface3'} borderColor={isDarkMode ? '$transparent' : '$surface3'}
borderRadius="$roundedFull" borderRadius="$roundedFull"
borderWidth={1} borderWidth="$spacing1"
p="$spacing16" p="$spacing16"
paddingEnd="$spacing24" paddingEnd="$spacing24"
backgroundColor={colors.DEP_backgroundOverlay.val} backgroundColor={colors.DEP_backgroundOverlay.val}
......
...@@ -8,7 +8,7 @@ import { AccountListQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__ge ...@@ -8,7 +8,7 @@ import { AccountListQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__ge
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' 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 { Account } from 'wallet/src/features/wallet/accounts/types'
const ADDRESS_ROW_HEIGHT = 40 const ADDRESS_ROW_HEIGHT = 40
...@@ -23,7 +23,7 @@ type Portfolio = NonNullable<NonNullable<NonNullable<AccountListQuery['portfolio ...@@ -23,7 +23,7 @@ type Portfolio = NonNullable<NonNullable<NonNullable<AccountListQuery['portfolio
function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element { function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element {
const { fullHeight } = useDeviceDimensions() const { fullHeight } = useDeviceDimensions()
const addresses = useMemo(() => accounts.map((account) => account.address), [accounts]) const addresses = useMemo(() => accounts.map((account) => account.address), [accounts])
const { data, loading } = useAccountList({ const { data, loading } = useAccountListData({
addresses, addresses,
notifyOnNetworkStatusChange: true, notifyOnNetworkStatusChange: true,
}) })
...@@ -57,7 +57,7 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele ...@@ -57,7 +57,7 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele
<Flex <Flex
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
maxHeight={accountsScrollViewHeight} maxHeight={accountsScrollViewHeight}
px="$spacing12" px="$spacing12"
width="100%" width="100%"
......
...@@ -74,7 +74,14 @@ export function DappConnectedNetworkModal({ session, onClose }: DappConnectedNet ...@@ -74,7 +74,14 @@ export function DappConnectedNetworkModal({ session, onClose }: DappConnectedNet
</Text> </Text>
</Flex> </Flex>
<Flex row> <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) => ( {session.chains.map((chainId) => (
<Flex key={chainId} row alignItems="center" justifyContent="space-between"> <Flex key={chainId} row alignItems="center" justifyContent="space-between">
<NetworkLogo chainId={chainId} size={iconSizes.icon24} /> <NetworkLogo chainId={chainId} size={iconSizes.icon24} />
......
...@@ -66,7 +66,7 @@ function KidSuperCheckinModalContent({ request }: { request: SignRequest }): JSX ...@@ -66,7 +66,7 @@ function KidSuperCheckinModalContent({ request }: { request: SignRequest }): JSX
centered centered
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
gap="$spacing12" gap="$spacing12"
px="$spacing24" px="$spacing24"
py="$spacing24" py="$spacing24"
......
...@@ -144,7 +144,7 @@ function TransactionDetails({ ...@@ -144,7 +144,7 @@ function TransactionDetails({
<Flex <Flex
borderColor={isLoading ? '$transparent' : '$surface3'} borderColor={isLoading ? '$transparent' : '$surface3'}
borderRadius="$rounded12" borderRadius="$rounded12"
borderWidth={1} borderWidth="$spacing1"
px="$spacing8" px="$spacing8"
py="$spacing2" py="$spacing2"
> >
......
...@@ -90,7 +90,7 @@ export function WalletConnectRequestModalContent({ ...@@ -90,7 +90,7 @@ export function WalletConnectRequestModalContent({
<> <>
<ClientDetails permitInfo={permitInfo} request={request} /> <ClientDetails permitInfo={permitInfo} request={request} />
<Flex pt="$spacing8"> <Flex pt="$spacing8">
<Flex backgroundColor="$surface2" borderColor="$surface3" borderRadius="$rounded16" borderWidth={1}> <Flex backgroundColor="$surface2" borderColor="$surface3" borderRadius="$rounded16" borderWidth="$spacing1">
{!permitInfo && ( {!permitInfo && (
<SectionContainer style={requestMessageStyle}> <SectionContainer style={requestMessageStyle}>
<RequestDetails request={request} /> <RequestDetails request={request} />
......
...@@ -58,7 +58,7 @@ const SitePermissions = (): JSX.Element => { ...@@ -58,7 +58,7 @@ const SitePermissions = (): JSX.Element => {
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
minHeight={44} minHeight={44}
p="$spacing12" p="$spacing12"
> >
......
...@@ -282,7 +282,7 @@ export function WalletConnectModal({ ...@@ -282,7 +282,7 @@ export function WalletConnectModal({
<TouchableArea <TouchableArea
borderColor={isDarkMode ? '$transparent' : '$surface3'} borderColor={isDarkMode ? '$transparent' : '$surface3'}
borderRadius="$roundedFull" borderRadius="$roundedFull"
borderWidth={1} borderWidth="$spacing1"
p="$spacing16" p="$spacing16"
paddingEnd="$spacing24" paddingEnd="$spacing24"
backgroundColor={colors.DEP_backgroundOverlay.val} backgroundColor={colors.DEP_backgroundOverlay.val}
......
...@@ -3,15 +3,41 @@ import { useTranslation } from 'react-i18next' ...@@ -3,15 +3,41 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { closeAllModals } from 'src/features/modals/modalSlice' import { closeAllModals } from 'src/features/modals/modalSlice'
import { DeprecatedButton, Flex, Text, useSporeColors } from 'ui/src' import { DeprecatedButton, Flex, useSporeColors } from 'ui/src'
import { WalletFilled } from 'ui/src/components/icons' import { ArrowDownCircle, WalletFilled } from 'ui/src/components/icons'
import { iconSizes, opacify } from 'ui/src/theme' 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 { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' 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 { export function RestoreWalletModal(): JSX.Element | null {
const { t } = useTranslation() const { t } = useTranslation()
const colors = useSporeColors() const colors = useSporeColors()
...@@ -29,27 +55,50 @@ export function RestoreWalletModal(): JSX.Element | null { ...@@ -29,27 +55,50 @@ export function RestoreWalletModal(): JSX.Element | null {
} }
return ( return (
<Modal hideHandlebar backgroundColor={colors.surface2.val} isDismissible={false} name={ModalName.RestoreWallet}> <Modal hideHandlebar backgroundColor={colors.surface1.val} isDismissible={false} name={ModalName.RestoreWallet}>
<Flex centered gap="$spacing16" px="$spacing24" py="$spacing12"> <Flex centered gap="$spacing24" px="$spacing24" py="$spacing12" backgroundColor="$surface1">
<Flex <Flex
centered centered
borderRadius="$roundedFull" width="100%"
p="$spacing12" height={CONTAINER_HEIGHT}
style={{ position="relative"
backgroundColor: opacify(12, colors.neutral1.val), 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} /> <WalletFilled color="$neutral1" size={iconSizes.icon24} />
<Flex position="absolute" bottom={ICON_OFFSET} right={ICON_OFFSET}>
<ArrowDownCircle color="$accent1" size={iconSizes.icon24} />
</Flex> </Flex>
<Text textAlign="center" variant="body1"> </Flex>
{t('account.wallet.button.restore')} </Flex>
</Text>
<Text color="$neutral2" textAlign="center" variant="body2"> <GenericHeader
{t('account.wallet.restore.description')} title={t('account.wallet.button.restore')}
</Text> titleVariant="subheading1"
<Flex centered row gap="$spacing12" pt="$spacing12"> subtitle={t('account.wallet.restore.description')}
<DeprecatedButton fill testID={TestID.RestoreWallet} theme="primary" onPress={onRestore}> subtitleVariant="body3"
{t('common.button.restore')} />
<Flex row>
<DeprecatedButton fill testID={TestID.RestoreWallet} theme="primary" size="medium" onPress={onRestore}>
{t('common.button.continue')}
</DeprecatedButton> </DeprecatedButton>
</Flex> </Flex>
</Flex> </Flex>
......
import { NetworkStatus } from '@apollo/client'
import { BottomSheetFlatList } from '@gorhom/bottom-sheet' import { BottomSheetFlatList } from '@gorhom/bottom-sheet'
import { useFocusEffect } from '@react-navigation/core' import { useFocusEffect } from '@react-navigation/core'
import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation'
...@@ -207,7 +208,7 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T ...@@ -207,7 +208,7 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
const HeaderComponent = memo(function _HeaderComponent(): JSX.Element | null { const HeaderComponent = memo(function _HeaderComponent(): JSX.Element | null {
const { t } = useTranslation() const { t } = useTranslation()
const { balancesById, networkStatus, refetch } = useTokenBalanceListContext() const { balancesById, networkStatus, refetch } = useTokenBalanceListContext()
const hasError = isError(networkStatus, !!balancesById) const hasError = !!balancesById && networkStatus === NetworkStatus.error
return hasError ? ( return hasError ? (
<AnimatedFlex entering={FadeInDown} exiting={FadeOut} px="$spacing24" py="$spacing8"> <AnimatedFlex entering={FadeInDown} exiting={FadeOut} px="$spacing24" py="$spacing8">
......
...@@ -14,7 +14,7 @@ import * as userSettingsHooks from 'uniswap/src/features/settings/hooks' ...@@ -14,7 +14,7 @@ import * as userSettingsHooks from 'uniswap/src/features/settings/hooks'
import { MobileUserPropertyName } from 'uniswap/src/features/telemetry/user' import { MobileUserPropertyName } from 'uniswap/src/features/telemetry/user'
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics' 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 * as walletHooks from 'wallet/src/features/wallet/hooks'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
...@@ -33,6 +33,11 @@ jest.mock('wallet/src/features/wallet/Keyring/Keyring', () => { ...@@ -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 mockDispatch = jest.fn()
const mockSelector = jest.fn() const mockSelector = jest.fn()
...@@ -51,19 +56,28 @@ const signerAccount1 = { ...@@ -51,19 +56,28 @@ const signerAccount1 = {
type: AccountType.SignerMnemonic, type: AccountType.SignerMnemonic,
address: address1, address: address1,
timeImportedMs: 100000, timeImportedMs: 100000,
} pushNotificationsEnabled: true,
mnemonicId: '111',
derivationIndex: 0,
} satisfies SignerMnemonicAccount
const signerAccount2 = { const signerAccount2 = {
type: AccountType.SignerMnemonic, type: AccountType.SignerMnemonic,
address: address2, address: address2,
timeImportedMs: 100000, timeImportedMs: 100000,
} pushNotificationsEnabled: true,
mnemonicId: '222',
derivationIndex: 1,
} satisfies SignerMnemonicAccount
const signerAccount3 = { const signerAccount3 = {
type: AccountType.SignerMnemonic, type: AccountType.SignerMnemonic,
address: address3, address: address3,
timeImportedMs: 100000, timeImportedMs: 100000,
} pushNotificationsEnabled: true,
mnemonicId: '333',
derivationIndex: 2,
} satisfies SignerMnemonicAccount
describe('TraceUserProperties', () => { describe('TraceUserProperties', () => {
afterEach(() => { afterEach(() => {
......
import { useEffect } from 'react' import { useEffect, useMemo } from 'react'
import { NativeModules } from 'react-native' import { NativeModules } from 'react-native'
import OneSignal from 'react-native-onesignal'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useBiometricAppSettings, useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks' import { useBiometricAppSettings, useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks'
import { OneSignalUserTagField } from 'src/features/notifications/constants'
import { getAuthMethod } from 'src/features/telemetry/utils' import { getAuthMethod } from 'src/features/telemetry/utils'
import { getFullAppVersion } from 'src/utils/version' import { getFullAppVersion } from 'src/utils/version'
import { useIsDarkMode } from 'ui/src' import { useIsDarkMode } from 'ui/src'
...@@ -11,10 +13,11 @@ import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' ...@@ -11,10 +13,11 @@ import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks'
import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks' import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks'
import { MobileUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user' import { MobileUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors'
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics' 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 { 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 { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { BackupType } from 'wallet/src/features/wallet/accounts/types'
import { import {
...@@ -39,6 +42,12 @@ export function TraceUserProperties(): null { ...@@ -39,6 +42,12 @@ export function TraceUserProperties(): null {
const hideSmallBalances = useHideSmallBalancesSetting() const hideSmallBalances = useHideSmallBalancesSetting()
const { isTestnetModeEnabled } = useEnabledChains() 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 // Effects must check this and ensure they are setting properties for when analytics is reenabled
const allowAnalytics = useSelector(selectAllowAnalytics) const allowAnalytics = useSelector(selectAllowAnalytics)
...@@ -70,12 +79,9 @@ export function TraceUserProperties(): null { ...@@ -70,12 +79,9 @@ export function TraceUserProperties(): null {
}, [allowAnalytics, isDarkMode]) }, [allowAnalytics, isDarkMode])
useEffect(() => { useEffect(() => {
setUserProperty(MobileUserPropertyName.WalletSignerCount, signerAccounts.length) setUserProperty(MobileUserPropertyName.WalletSignerCount, signerAccountAddresses.length)
setUserProperty( setUserProperty(MobileUserPropertyName.WalletSignerAccounts, signerAccountAddresses)
MobileUserPropertyName.WalletSignerAccounts, }, [allowAnalytics, signerAccountAddresses])
signerAccounts.map((account) => account.address),
)
}, [allowAnalytics, signerAccounts])
useEffect(() => { useEffect(() => {
setUserProperty(MobileUserPropertyName.WalletViewOnlyCount, viewOnlyAccounts.length) setUserProperty(MobileUserPropertyName.WalletViewOnlyCount, viewOnlyAccounts.length)
...@@ -117,5 +123,9 @@ export function TraceUserProperties(): null { ...@@ -117,5 +123,9 @@ export function TraceUserProperties(): null {
setUserProperty(MobileUserPropertyName.TestnetModeEnabled, isTestnetModeEnabled) setUserProperty(MobileUserPropertyName.TestnetModeEnabled, isTestnetModeEnabled)
}, [allowAnalytics, isTestnetModeEnabled]) }, [allowAnalytics, isTestnetModeEnabled])
useEffect(() => {
OneSignal.sendTag(OneSignalUserTagField.AccountIsUnfunded, signerAccountsTotalBalance === 0 ? 'true' : 'false')
}, [signerAccountsTotalBalance])
return null return null
} }
...@@ -2,11 +2,11 @@ import { AccountCardItem } from 'src/components/accounts/AccountCardItem' ...@@ -2,11 +2,11 @@ import { AccountCardItem } from 'src/components/accounts/AccountCardItem'
import { fireEvent, render, screen, waitFor } from 'src/test/test-utils' 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 { ON_PRESS_EVENT_PAYLOAD, SAMPLE_SEED_ADDRESS_1, amount, portfolio } from 'uniswap/src/test/fixtures'
import { queryResolvers } from 'uniswap/src/test/utils' 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, () => { describe(AccountCardItem, () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(hooks, 'useAccountList').mockReturnValue({ jest.spyOn(hooks, 'useAccountListData').mockReturnValue({
data: undefined, data: undefined,
loading: false, loading: false,
networkStatus: 7, networkStatus: 7,
......
...@@ -18,7 +18,7 @@ import { MobileScreens } from 'uniswap/src/types/screens/mobile' ...@@ -18,7 +18,7 @@ import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { setClipboard } from 'uniswap/src/utils/clipboard' import { setClipboard } from 'uniswap/src/utils/clipboard'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' 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 = { type AccountCardItemProps = {
address: Address address: Address
...@@ -44,7 +44,7 @@ function PortfolioValue({ ...@@ -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. // 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. // 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', fetchPolicy: 'cache-first',
addresses: [address], addresses: [address],
}) })
......
...@@ -10,7 +10,7 @@ import { PollingInterval } from 'uniswap/src/constants/misc' ...@@ -10,7 +10,7 @@ import { PollingInterval } from 'uniswap/src/constants/misc'
import { AccountType } from 'uniswap/src/features/accounts/types' import { AccountType } from 'uniswap/src/features/accounts/types'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' 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' import { Account } from 'wallet/src/features/wallet/accounts/types'
type AccountListProps = Pick<ComponentProps<typeof AccountCardItem>, 'onPress'> & { type AccountListProps = Pick<ComponentProps<typeof AccountCardItem>, 'onPress'> & {
...@@ -63,7 +63,7 @@ export function AccountList({ accounts, onPress, isVisible }: AccountListProps): ...@@ -63,7 +63,7 @@ export function AccountList({ accounts, onPress, isVisible }: AccountListProps):
const colors = useSporeColors() const colors = useSporeColors()
const addresses = useMemo(() => accounts.map((a) => a.address), [accounts]) const addresses = useMemo(() => accounts.map((a) => a.address), [accounts])
const { data, networkStatus, refetch, startPolling, stopPolling } = useAccountList({ const { data, networkStatus, refetch, startPolling, stopPolling } = useAccountListData({
addresses, addresses,
notifyOnNetworkStatusChange: true, notifyOnNetworkStatusChange: true,
}) })
......
...@@ -27,6 +27,7 @@ exports[`AccountList renders without error 1`] = ` ...@@ -27,6 +27,7 @@ exports[`AccountList renders without error 1`] = `
"derivationIndex": 0, "derivationIndex": 0,
"mnemonicId": "0x82D56A352367453f74FC0dC7B071b311da373Fa6", "mnemonicId": "0x82D56A352367453f74FC0dC7B071b311da373Fa6",
"name": "Test Account", "name": "Test Account",
"pushNotificationsEnabled": true,
"timeImportedMs": 10, "timeImportedMs": 10,
"type": "signerMnemonic", "type": "signerMnemonic",
}, },
......
...@@ -33,7 +33,7 @@ export function BottomBanner({ text, icon, backgroundColor, translateY }: Bottom ...@@ -33,7 +33,7 @@ export function BottomBanner({ text, icon, backgroundColor, translateY }: Bottom
backgroundColor={backgroundColor ? backgroundColor : '$accent1'} backgroundColor={backgroundColor ? backgroundColor : '$accent1'}
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded8" borderRadius="$rounded8"
borderWidth={1} borderWidth="$spacing1"
bottom={0} bottom={0}
entering={FadeIn} entering={FadeIn}
exiting={FadeOut} exiting={FadeOut}
......
...@@ -208,7 +208,7 @@ function AllNetworksPill({ onPress, selected }: { onPress: () => void; selected: ...@@ -208,7 +208,7 @@ function AllNetworksPill({ onPress, selected }: { onPress: () => void; selected:
backgroundColor={selected ? '$surface3' : '$surface1'} backgroundColor={selected ? '$surface3' : '$surface1'}
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded12" borderRadius="$rounded12"
borderWidth={1} borderWidth="$spacing1"
gap="$spacing8" gap="$spacing8"
pl="$spacing4" pl="$spacing4"
pr="$spacing12" pr="$spacing12"
......
...@@ -165,7 +165,7 @@ function ServiceProviderLogo({ uri }: { uri: string }): JSX.Element { ...@@ -165,7 +165,7 @@ function ServiceProviderLogo({ uri }: { uri: string }): JSX.Element {
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface1" borderColor="$surface1"
borderRadius="$rounded8" borderRadius="$rounded8"
borderWidth={2} borderWidth="$spacing2"
overflow="hidden" overflow="hidden"
> >
<ImageUri <ImageUri
...@@ -187,7 +187,7 @@ function ReceiveCryptoIcon(): JSX.Element { ...@@ -187,7 +187,7 @@ function ReceiveCryptoIcon(): JSX.Element {
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface1" borderColor="$surface1"
borderRadius="$roundedFull" borderRadius="$roundedFull"
borderWidth={1} borderWidth="$spacing1"
overflow="hidden" overflow="hidden"
> >
<ArrowDownCircle color="$accent1" size="$icon.24" /> <ArrowDownCircle color="$accent1" size="$icon.24" />
......
...@@ -5,11 +5,18 @@ import { useDispatch, useSelector } from 'react-redux' ...@@ -5,11 +5,18 @@ import { useDispatch, useSelector } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal' import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import {
NotificationPermission,
useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { Flex } from 'ui/src' 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 { UnichainIntroModal } from 'uniswap/src/components/unichain/UnichainIntroModal'
import { AccountType } from 'uniswap/src/features/accounts/types' import { AccountType } from 'uniswap/src/features/accounts/types'
import { UniverseChainId } from 'uniswap/src/features/chains/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 { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types' import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
...@@ -25,8 +32,8 @@ import { ...@@ -25,8 +32,8 @@ import {
import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack' import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack'
import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards' import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { selectHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/selectors' import { selectHasViewedNotificationsCard } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/slice' import { setHasViewedNotificationsCard } from 'wallet/src/features/behaviorHistory/slice'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
type OnboardingIntroCardStackProps = { type OnboardingIntroCardStackProps = {
...@@ -44,8 +51,13 @@ export function OnboardingIntroCardStack({ ...@@ -44,8 +51,13 @@ export function OnboardingIntroCardStack({
const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic
const hasBackups = activeAccount.backups && activeAccount.backups.length > 0 const hasBackups = activeAccount.backups && activeAccount.backups.length > 0
const welcomeCardTitle = t('onboarding.home.intro.welcome.title') const { notificationPermissionsEnabled } = useNotificationOSPermissionsEnabled()
const hasViewedWelcomeWalletCard = useSelector(selectHasViewedWelcomeWalletCard) const notificationOnboardingCardEnabled = useFeatureFlag(FeatureFlags.NotificationOnboardingCard)
const hasViewedNotificationsCard = useSelector(selectHasViewedNotificationsCard)
const showEnableNotificationsCard =
notificationOnboardingCardEnabled &&
notificationPermissionsEnabled === NotificationPermission.Disabled &&
!hasViewedNotificationsCard
const { navigateToSwapFlow } = useWalletNavigation() const { navigateToSwapFlow } = useWalletNavigation()
...@@ -128,28 +140,30 @@ export function OnboardingIntroCardStack({ ...@@ -128,28 +140,30 @@ export function OnboardingIntroCardStack({
output.push(...sharedCards) output.push(...sharedCards)
if (output.length && !hasViewedWelcomeWalletCard) { if (showEnableNotificationsCard) {
output.unshift({ output.push({
loggingName: OnboardingCardLoggingName.WelcomeWallet, loggingName: OnboardingCardLoggingName.EnablePushNotifications,
graphic: { graphic: {
type: IntroCardGraphicType.Icon, type: IntroCardGraphicType.Image,
Icon: UniswapLogo, image: PUSH_NOTIFICATIONS_CARD_BANNER,
iconProps: {
color: '$accent1',
}, },
iconContainerProps: { title: t('onboarding.home.intro.pushNotifications.title'),
backgroundColor: '$accent2', description: t('onboarding.home.intro.pushNotifications.description'),
borderRadius: '$rounded12', 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 return output
}, [hasBackups, showEmptyWalletState, hasViewedWelcomeWalletCard, isSignerAccount, sharedCards, t, welcomeCardTitle]) }, [hasBackups, showEmptyWalletState, isSignerAccount, sharedCards, t, showEnableNotificationsCard, dispatch])
const handleSwiped = useCallback( const handleSwiped = useCallback(
(_card: IntroCardProps, index: number) => { (_card: IntroCardProps, index: number) => {
...@@ -159,12 +173,8 @@ export function OnboardingIntroCardStack({ ...@@ -159,12 +173,8 @@ export function OnboardingIntroCardStack({
card_name: loggingName, card_name: loggingName,
}) })
} }
if (!hasViewedWelcomeWalletCard && cards[index]?.title === welcomeCardTitle) {
dispatch(setHasViewedWelcomeWalletCard(true))
}
}, },
[cards, dispatch, hasViewedWelcomeWalletCard, welcomeCardTitle], [cards],
) )
const UnichainIntroModalInstance = useMemo((): JSX.Element => { const UnichainIntroModalInstance = useMemo((): JSX.Element => {
......
...@@ -24,7 +24,7 @@ export const PasswordInput = forwardRef<NativeTextInput, TextInputProps>(functio ...@@ -24,7 +24,7 @@ export const PasswordInput = forwardRef<NativeTextInput, TextInputProps>(functio
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
p="$spacing4" p="$spacing4"
> >
<AnimatedFlex fill grow row alignItems="center" minHeight={48}> <AnimatedFlex fill grow row alignItems="center" minHeight={48}>
...@@ -34,7 +34,7 @@ export const PasswordInput = forwardRef<NativeTextInput, TextInputProps>(functio ...@@ -34,7 +34,7 @@ export const PasswordInput = forwardRef<NativeTextInput, TextInputProps>(functio
autoCorrect={false} autoCorrect={false}
backgroundColor="$transparent" backgroundColor="$transparent"
blurOnSubmit={false} blurOnSubmit={false}
borderWidth={0} borderWidth="$none"
clearTextOnFocus={false} clearTextOnFocus={false}
flex={1} flex={1}
fontFamily="$subHeading" fontFamily="$subHeading"
......
...@@ -20,7 +20,7 @@ export function SelectionCircle({ ...@@ -20,7 +20,7 @@ export function SelectionCircle({
centered centered
borderColor={selected ? selectedColor : unselectedColor} borderColor={selected ? selectedColor : unselectedColor}
borderRadius="$roundedFull" borderRadius="$roundedFull"
borderWidth={1} borderWidth="$spacing1"
height={iconSizes[size]} height={iconSizes[size]}
width={iconSizes[size]} width={iconSizes[size]}
> >
......
...@@ -24,7 +24,7 @@ export function HiddenMnemonicWordView({ ...@@ -24,7 +24,7 @@ export function HiddenMnemonicWordView({
backgroundColor="$surface2" backgroundColor="$surface2"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
gap="$spacing36" gap="$spacing36"
px="$spacing32" px="$spacing32"
py="$spacing24" py="$spacing24"
...@@ -41,7 +41,7 @@ export function HiddenMnemonicWordView({ ...@@ -41,7 +41,7 @@ export function HiddenMnemonicWordView({
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={1} borderWidth="$spacing1"
gap="$spacing4" gap="$spacing4"
paddingEnd="$spacing16" paddingEnd="$spacing16"
paddingStart="$spacing12" paddingStart="$spacing12"
......
...@@ -17,7 +17,7 @@ function _NotificationBadge({ children, address }: Props): JSX.Element { ...@@ -17,7 +17,7 @@ function _NotificationBadge({ children, address }: Props): JSX.Element {
backgroundColor="$accent1" backgroundColor="$accent1"
borderColor="$surface2" borderColor="$surface2"
borderRadius="$roundedFull" borderRadius="$roundedFull"
borderWidth={2} borderWidth="$spacing2"
height={NOTIFICATION_DOT_SIZE} height={NOTIFICATION_DOT_SIZE}
position="absolute" position="absolute"
right={-NOTIFICATION_DOT_SIZE / 4} right={-NOTIFICATION_DOT_SIZE / 4}
......
...@@ -36,8 +36,6 @@ function* _handleOffRampReturnLink(url: URL) { ...@@ -36,8 +36,6 @@ function* _handleOffRampReturnLink(url: URL) {
throw new Error('Missing externalTransactionId or moonpay data in fiat offramp deep link') throw new Error('Missing externalTransactionId or moonpay data in fiat offramp deep link')
} }
sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampWidgetCompleted, { externalTransactionId })
let offRampTransferDetails: OffRampTransferDetailsResponse | undefined let offRampTransferDetails: OffRampTransferDetailsResponse | undefined
try { try {
...@@ -51,6 +49,7 @@ function* _handleOffRampReturnLink(url: URL) { ...@@ -51,6 +49,7 @@ function* _handleOffRampReturnLink(url: URL) {
} catch (error) { } catch (error) {
logger.error(error, { logger.error(error, {
tags: { file: 'handleOffRampReturnLinkSaga', function: 'handleOffRampReturnLink' }, tags: { file: 'handleOffRampReturnLinkSaga', function: 'handleOffRampReturnLink' },
extra: { url: url.toString() },
}) })
throw new Error('Failed to fetch offramp transfer details') throw new Error('Failed to fetch offramp transfer details')
} }
...@@ -62,6 +61,16 @@ function* _handleOffRampReturnLink(url: URL) { ...@@ -62,6 +61,16 @@ function* _handleOffRampReturnLink(url: URL) {
const { tokenAddress, baseCurrencyCode, baseCurrencyAmount, depositWalletAddress, logos, provider, chainId } = const { tokenAddress, baseCurrencyCode, baseCurrencyAmount, depositWalletAddress, logos, provider, chainId } =
offRampTransferDetails offRampTransferDetails
const analyticsProperties = {
cryptoCurrency: baseCurrencyCode,
currencyAmount: baseCurrencyAmount,
serviceProvider: provider,
chainId,
externalTransactionId,
}
sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampWidgetCompleted, analyticsProperties)
const currencyTradeableAsset: TradeableAsset = { const currencyTradeableAsset: TradeableAsset = {
address: tokenAddress, address: tokenAddress,
chainId: Number(chainId) as UniverseChainId, chainId: Number(chainId) as UniverseChainId,
...@@ -71,14 +80,8 @@ function* _handleOffRampReturnLink(url: URL) { ...@@ -71,14 +80,8 @@ function* _handleOffRampReturnLink(url: URL) {
const fiatOffRampMetaData: FiatOffRampMetaData = { const fiatOffRampMetaData: FiatOffRampMetaData = {
name: provider, name: provider,
logoUrl: logos.lightLogo, logoUrl: logos.lightLogo,
onSubmitCallback: () => { onSubmitCallback: (amountUSD?: number) => {
sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampFundsSent, { sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampFundsSent, { ...analyticsProperties, amountUSD })
cryptoCurrency: baseCurrencyCode,
currencyAmount: baseCurrencyAmount,
serviceProvider: provider,
chainId,
externalTransactionId,
})
}, },
moonpayCurrencyCode: baseCurrencyCode, moonpayCurrencyCode: baseCurrencyCode,
meldCurrencyCode: baseCurrencyCode, meldCurrencyCode: baseCurrencyCode,
......
...@@ -217,7 +217,7 @@ export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHea ...@@ -217,7 +217,7 @@ export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHea
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
height={46} height={46}
p="$spacing12" p="$spacing12"
shadowColor="$neutral1" shadowColor="$neutral1"
...@@ -232,7 +232,7 @@ export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHea ...@@ -232,7 +232,7 @@ export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHea
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
height={46} height={46}
justifyContent="center" justifyContent="center"
px="$spacing12" px="$spacing12"
......
...@@ -234,7 +234,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi ...@@ -234,7 +234,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
autoFocus autoFocus
alignSelf="stretch" alignSelf="stretch"
backgroundColor="$transparent" backgroundColor="$transparent"
borderWidth={0} borderWidth="$none"
disabled={disabled} disabled={disabled}
fiatCurrencyInfo={fiatCurrencyInfo} fiatCurrencyInfo={fiatCurrencyInfo}
fontFamily="$heading" fontFamily="$heading"
......
...@@ -122,7 +122,7 @@ export function GenericImportForm({ ...@@ -122,7 +122,7 @@ export function GenericImportForm({
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor={borderColor} borderColor={borderColor}
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
minHeight={shouldUseMinHeight ? INPUT_MIN_HEIGHT : undefined} minHeight={shouldUseMinHeight ? INPUT_MIN_HEIGHT : undefined}
px="$spacing24" px="$spacing24"
py="$spacing16" py="$spacing16"
......
import { Linking } from 'react-native' import { Linking } from 'react-native'
import OneSignal, { NotificationReceivedEvent, OpenedEvent } from 'react-native-onesignal' import OneSignal, { NotificationReceivedEvent, OpenedEvent } from 'react-native-onesignal'
import { NotificationType } from 'src/features/notifications/constants'
import { config } from 'uniswap/src/config' 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 { 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 { logger } from 'utilities/src/logger/logger'
import { isIOS } from 'utilities/src/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient' import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient'
...@@ -10,8 +15,34 @@ export const initOneSignal = (): void => { ...@@ -10,8 +15,34 @@ export const initOneSignal = (): void => {
OneSignal.setAppId(config.onesignalAppId) OneSignal.setAppId(config.onesignalAppId)
OneSignal.setNotificationWillShowInForegroundHandler((event: NotificationReceivedEvent) => { 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 // 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) => { OneSignal.setNotificationOpenedHandler((event: OpenedEvent) => {
...@@ -32,6 +63,21 @@ export const initOneSignal = (): void => { ...@@ -32,6 +63,21 @@ export const initOneSignal = (): void => {
Linking.emit('url', { url: event.notification.launchURL }) 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> => { 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 { useMutation } from '@tanstack/react-query'
import { useCallback, useEffect, useState } from 'react' 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 { promptPushPermission } from 'src/features/notifications/Onesignal'
import { NotifSettingType } from 'src/features/notifications/constants'
import { import {
NotificationPermission, NotificationPermission,
useNotificationOSPermissionsEnabled, useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/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 { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks' import { useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks'
...@@ -55,21 +57,21 @@ export function useSettingNotificationToggle({ ...@@ -55,21 +57,21 @@ export function useSettingNotificationToggle({
type: NotifSettingType type: NotifSettingType
onToggle?: (enabled: boolean) => void onToggle?: (enabled: boolean) => void
}): ReturnType<typeof useBaseNotificationToggle> { }): ReturnType<typeof useBaseNotificationToggle> {
const [isAppPermissionEnabled, setAppPermissionEnabled] = useState(false) const dispatch = useDispatch()
const { generalUpdatesEnabled, priceAlertsEnabled } = useSelector(selectAllPushNotificationSettings)
useEffect(() => { const permissionEnabledMap: Record<NotifSettingType, boolean> = {
getNotifSetting(type) [NotifSettingType.GeneralUpdates]: generalUpdatesEnabled,
.then(setAppPermissionEnabled) [NotifSettingType.PriceAlerts]: priceAlertsEnabled,
.catch(() => {}) }
}, [type]) const isAppPermissionEnabled = permissionEnabledMap[type]
const handleToggle = useCallback( const handleToggle = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
handleNotifSettingToggled(type, enabled) dispatch(updateNotifSettings({ [type]: enabled }))
setAppPermissionEnabled(enabled)
onToggle?.(enabled) onToggle?.(enabled)
}, },
[onToggle, type], [dispatch, onToggle, type],
) )
return useBaseNotificationToggle({ isAppPermissionEnabled, onToggle: handleToggle }) 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 { ...@@ -102,7 +102,7 @@ function CloudBackupPreview(): JSX.Element {
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded12" borderRadius="$rounded12"
borderWidth={1} borderWidth="$spacing1"
gap="$spacing16" gap="$spacing16"
px="$spacing12" px="$spacing12"
py="$spacing8" py="$spacing8"
......
...@@ -30,7 +30,7 @@ export function LockPreviewImage({ height = DEFAULT_PREVIEW_HEIGHT }: { height?: ...@@ -30,7 +30,7 @@ export function LockPreviewImage({ height = DEFAULT_PREVIEW_HEIGHT }: { height?:
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded12" borderRadius="$rounded12"
borderWidth={1} borderWidth="$spacing1"
height={BOXES_CONTAINER_HEIGHT} height={BOXES_CONTAINER_HEIGHT}
position="relative" position="relative"
pt="$spacing16" pt="$spacing16"
...@@ -58,7 +58,7 @@ export function LockPreviewImage({ height = DEFAULT_PREVIEW_HEIGHT }: { height?: ...@@ -58,7 +58,7 @@ export function LockPreviewImage({ height = DEFAULT_PREVIEW_HEIGHT }: { height?:
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded12" borderRadius="$rounded12"
borderWidth={1} borderWidth="$spacing1"
p="$spacing12" p="$spacing12"
top="$spacing24" top="$spacing24"
> >
......
...@@ -34,7 +34,7 @@ export function OptionCard({ ...@@ -34,7 +34,7 @@ export function OptionCard({
backgroundColor={isDarkMode ? '$surface2' : '$surface1'} backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
disabled={disabled} disabled={disabled}
opacity={disabled ? 0.5 : opacity} opacity={disabled ? 0.5 : opacity}
p="$spacing16" p="$spacing16"
......
import { SharedEventName } from '@uniswap/analytics-events' import { SharedEventName } from '@uniswap/analytics-events'
import OneSignal from 'react-native-onesignal'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app/navigation/types' 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 { MobileAppsFlyerEvents } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent, sendAppsFlyerEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent, sendAppsFlyerEvent } from 'uniswap/src/features/telemetry/send'
import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { logger } from 'utilities/src/logger/logger' 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 { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice'
...@@ -25,12 +29,20 @@ export function useCompleteOnboardingCallback({ ...@@ -25,12 +29,20 @@ export function useCompleteOnboardingCallback({
const navigation = useOnboardingStackNavigation() const navigation = useOnboardingStackNavigation()
const onboardingAccounts = getAllOnboardingAccounts() const onboardingAccounts = getAllOnboardingAccounts()
const onboardingAddresses = Object.keys(onboardingAccounts) const onboardingAddresses = onboardingAccounts.map((account) => account.address)
return async () => { return async () => {
// Run all shared onboarding completion logic // Run all shared onboarding completion logic
await finishOnboarding({ importType }) 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 // Send appsflyer event for mobile attribution
if (entryPoint === OnboardingEntryPoint.FreshInstallOrReplace) { if (entryPoint === OnboardingEntryPoint.FreshInstallOrReplace) {
sendAppsFlyerEvent(MobileAppsFlyerEvents.OnboardingCompleted, { importType }).catch((error) => sendAppsFlyerEvent(MobileAppsFlyerEvents.OnboardingCompleted, { importType }).catch((error) =>
......
...@@ -82,7 +82,7 @@ export function AIAssistantScreen(): JSX.Element { ...@@ -82,7 +82,7 @@ export function AIAssistantScreen(): JSX.Element {
backgroundColor="$surface1" backgroundColor="$surface1"
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth="$spacing1"
mx="$spacing16" mx="$spacing16"
> >
<Input <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