ci(release): publish latest release

parent 30ff2ecc
* @uniswap/web-admins
We are back with another (small) round of updates. Check out what is new below:
IPFS hash of the deployment:
- CIDv0: `QmX4W6sgCHYCcR4SxeJVbZnj5N1GXR2ERPJomoBjVSbZVZ`
- CIDv1: `bafybeiebsyucxoiaieyyktifwl7glbepaklj4tncplc3rvssq55rpl553a`
Token Details Page Improvements — We took a pass at simplifying the core flows of token detail pages. We clarified the language around contract addresses and made it simpler than ever to copy them to your clipboard. In addition, we added a quick and easy route to our ‘receive’ flow from any given token details page. Share (and send) tokens easier than ever with our app!
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
You can also access the Uniswap Interface from an IPFS gateway.
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeiebsyucxoiaieyyktifwl7glbepaklj4tncplc3rvssq55rpl553a.ipfs.dweb.link/
- https://bafybeiebsyucxoiaieyyktifwl7glbepaklj4tncplc3rvssq55rpl553a.ipfs.cf-ipfs.com/
- [ipfs://QmX4W6sgCHYCcR4SxeJVbZnj5N1GXR2ERPJomoBjVSbZVZ/](ipfs://QmX4W6sgCHYCcR4SxeJVbZnj5N1GXR2ERPJomoBjVSbZVZ/)
## 5.13.0 (2024-02-27)
### Features
* **web:** [info] Add Bottom Bar to PDP mWeb (#6444) edd221d
* **web:** [info] add tvl and volume % change to PDP query (#6453) 3054897
* **web:** [info] add warning label to swap modal on PDP page (#6398) f7bacf9
* **web:** [info] candlestick data integration and fallback refactor (#6510) 1136e54
* **web:** [info] candlestick tooltip (#6355) fbb184f
* **web:** [info] Connect PDP data to our BE (#6357) 45a32d6
* **web:** [info] Connect PDP Volume Chart to real data (#6455) 3c7fa86
* **web:** [info] Integrate BE Pool TX queries (#6424) d2df578
* **web:** [info] Migrate Explore and TDP TX tables to our GQL BE (#6502) b674b85
* **web:** [info] pdp loading states (#6504) 57c0279
* **web:** [info] Sortable Token Explore Table Headers (#6435) f0e95d0
* **web:** [info] TDP chart loading states (#6450) d45a2e7
* **web:** [info] use custom PDP seo page title (#6422) f2e6552
* **web:** [info] use dropdown bottom sheet in smaller screens (#6452) 0177aaa
* **web:** [info] Use real price data for PDP Price Chart (#6457) 9493135
* **web:** [info] Various mWeb polish (#6549) 97e313e
* **web:** [uni-tags] adding username to send (#6358) 507bffc
* **web:** [uni-tags] improve Web3Status and account drawer header (#6454) 0c57325
* **web:** [uni-tags] update banner copy and responsive styles (#6401) b8445de
* **web:** add Explore and TDP chart staleness checks (#6458) 71cac56
* **web:** add gas estimate to limits dialog (#6233) 6689ae4
* **web:** add more analytics for limits (#6475) 14de73d
* **web:** Add more limits analytics events (#6496) 1bc5245
* **web:** Add off chain order type to trade events and swap tab click event (#6417) 3ad9ce0
* **web:** add price change speedbump to limit form (#6447) 1f855b5
* **web:** change input currency from LimitPriceInputPanel (#6250) 8313cb9
* **web:** fix limit status updating (#6363) 727de8b
* **web:** handle remote open limits (#6407) 519dbb9
* **web:** invert limit price preset button adjustments (#6518) c93f355
* **web:** mv limits cta to activity tab (#6532) e34eb1c
* **web:** pending states for cancelling limits (#6360) 1a3f5b9
* **web:** redesign limits menu rows (#6575) 11ac692
* **web:** Set default quote currency in limits price display based on stablecoin (#6456) 4b5a48a
* **web:** sync url to swap tab (#6432) ffb2d8c
* **web:** transaction details for insufficient liquidity (#6531) 8d85c98
* **web:** Update progress indicator step titles for limit orders (#6470) 090248e
* **web:** update TOS last updated to 2/16/2024 (#6550) 533b087
* **web:** Use GQL Token Data for Pool Token Logos (#6505) 00e6504
### Bug Fixes
* **web:** [info] add tooltip for turnover on token table (#6507) 50ea4f5
* **web:** [info] block explorer icon not showing dark mode TDP (#6557) f0ca33d
* **web:** [info] explore table mobile hover (#6512) 7abb6f7
* **web:** [info] fix extralong numbers on explore chart section (#6524) db5bbdc
* **web:** [info] fix translations for chart type selector (#6533) ee54af9
* **web:** [info] flickering volume bars (#6581) 60582ac
* **web:** [info] hide chart y-axis on smaller screen (#6506) c64a8f4
* **web:** [info] knock out a bunch of high-pri polish tasks (#6305) e5d0797
* **web:** [info] make return to top clickable again (#6559) 6d65932
* **web:** [info] MOOREE polish (#6486) 0a5b22f
* **web:** [info] more polish (#6434) 41ca7ea
* **web:** [info] pdp matching tdp (#6501) fb8ec9d
* **web:** [info] Prevent infinite loading for fully loaded tables (#6394) 4233a2f
* **web:** [info] pull decimal data for gql tokens (#6558) 3d9a336
* **web:** [info] Show explorer icon in PDP Links in darkmode (#6584) 8bd04e5
* **web:** [info] Show PDP balances on mWeb (#6585) aee363f
* **web:** [info] Show TDP Bottom Bar at exactly 1024px (#6576) d04dd12
* **web:** [limits] empty transaction details with ETH (#6519) 8c1532d
* **web:** [limits] fix transaction details text to match design (#6534) dc4dbd3
* **web:** [limits] making market button the same as percentage (#6516) 6f417c9
* **web:** [limits] update order confirmation flow (#6490) 0966070
* **web:** [uni-tags] inline banner height, icon size, pfp (#6538) 67b10ab
* **web:** align swap box and action buttons (#6497) 7c846bc
* **web:** chain switching search params (#6491) 6c88199
* **web:** chain switching search params (#6491) (#6514) f10d64e
* **web:** Change auction period secs to 0 (#6589) eab18eb
* **web:** clear limits form when submitting a limit (#6515) 9bab70b
* **web:** color code buy/sell on TDP tx table (#6498) eaafcb6
* **web:** convert backend timestamp from sec to ms for X orders (#6540) 4161221
* **web:** de-crowd the swap header bar (#6546) b0fd755
* **web:** disable test and update gql schema (#6517) e3aa751
* **web:** disable text select on the $ in the send form (#6433) 2a9ba50
* **web:** division by zero error in limits menu (#6561) 3190502
* **web:** do not truncate token symbols (#6495) 3f3b28b
* **web:** enable limits for all tokens (#6485) ee6fd22
* **web:** fix cancelation confirmation modal for limit orders (#6583) 8565065
* **web:** fix limit tab breaking after switching to l2s (#6554) 15f2d32
* **web:** ignore touchmove on charts (#6500) 51c7d32
* **web:** invert displayed custom price adjustment to match presets (#6560) a9fbe5c
* **web:** layout bug in token selector (#6484) a4e2de1
* **web:** limit form styling nits (#6253) ca34460
* **web:** limiting max decimals in input (#6446) df01d65
* **web:** limits menu styling nits (#6254) a83faa5
* **web:** lint error in useCurrentPriceAdjustment (#6492) cd1e011
* **web:** long pool name overlap (#6503) 78d4d8c
* **web:** minor design nits for limits (#6522) 450366a
* **web:** Misc. design fixes to Token Detail Page (#6565) 7146990
* **web:** open limits menu button bug (#6389) 7cdcc98
* **web:** revert - clear limits form when submitting a limit (#6515) (#6528) 52ef6fd
* **web:** setState error from LimitForm (#6249) df2aca3
* **web:** stacked tvl rendering overlap (#6573) f090a13
* **web:** styling fixes for limits flow (#6525) 72ed86b
* **web:** swap header<>url navigation bug (#6513) ad8d8d9
* **web:** swap z-indexing (#6499) 1573567
* **web:** truncate currency amount to decimals when parsing (#6586) 2a891d9
* **web:** Use local activities if GraphQL assetActivities query returns nothing (#6494) 137b253
### Code Refactoring
* **web:** [info] tdp provider pattern (#6385) 5c52db7
* **web:** [info] use tdp context in subcomponents (#6400) 99bbedc
Other notable changes:
- Gas estimation and approval bug fix
- Updated Twitter icons to ‘X’
- Unsupported language bug fix
- Android bug fixes
mobile/1.21.1
\ No newline at end of file
web/5.13.0
\ No newline at end of file
......@@ -2,6 +2,8 @@
#
.DS_Store
.tamagui
# Xcode
#
build/
......
......@@ -125,17 +125,17 @@ android {
dev {
isDefault(true)
applicationIdSuffix ".dev"
versionName "1.21.1"
versionName "1.22"
dimension "variant"
}
beta {
applicationIdSuffix ".beta"
versionName "1.21.1"
versionName "1.22"
dimension "variant"
}
prod {
dimension "variant"
versionName "1.21.1"
versionName "1.22"
}
}
......@@ -199,6 +199,10 @@ dependencies {
implementation 'com.google.android.play:integrity:1.2.0'
// Firebase App Check: Import the BoM for the Firebase platform
implementation(platform("com.google.firebase:firebase-bom:32.7.2"))
implementation("com.google.firebase:firebase-appcheck-playintegrity")
// Guava
implementation "com.google.guava:guava:24.1-jre"
// Guava fix
......
......@@ -5,6 +5,16 @@ const inProduction = NODE_ENV === 'production'
module.exports = function (api) {
api.cache.using(() => process.env.NODE_ENV)
var plugins = [
// disable for now as its causing ci to hang
// process.env.NODE_ENV === 'test'
// ? null
// : [
// '@tamagui/babel-plugin',
// {
// components: ['ui'],
// config: '../../packages/ui/src/tamagui.config.ts',
// },
// ],
[
'module:react-native-dotenv',
{
......@@ -30,7 +40,7 @@ module.exports = function (api) {
'@babel/plugin-proposal-numeric-separator',
// automatically require React when using JSX
'react-require',
]
].filter(Boolean)
if (inProduction) {
// Remove all console statements in production
......@@ -38,6 +48,10 @@ module.exports = function (api) {
}
return {
ignore: [
// speeds up compile
'**/@tamagui/**/dist/**',
],
presets: ['module:metro-react-native-babel-preset'],
plugins,
}
......
......@@ -644,10 +644,6 @@ PODS:
- BoringSSL-GRPC/Implementation (0.0.24):
- BoringSSL-GRPC/Interface (= 0.0.24)
- BoringSSL-GRPC/Interface (0.0.24)
- Burnt (0.11.4):
- ExpoModulesCore
- SPAlert
- SPIndicator
- DoubleConversion (1.1.6)
- EthersRS (0.0.5)
- EXApplication (5.1.1):
......@@ -1339,8 +1335,6 @@ PODS:
- Sentry/HybridSDK (8.7.1):
- SentryPrivate (= 8.7.1)
- SentryPrivate (8.7.1)
- SPAlert (4.2.0)
- SPIndicator (1.6.4)
- UIImageColors (2.1.0)
- Yoga (1.14.0)
- ZXingObjC/Core (3.6.5)
......@@ -1354,7 +1348,6 @@ DEPENDENCIES:
- Apollo (= 1.2.1)
- Argon2Swift (= 1.0.3)
- boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`)
- Burnt (from `../../../node_modules/burnt/ios`)
- DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- "EthersRS (from `../../../node_modules/@uniswap/ethers-rs-mobile`)"
- EXApplication (from `../../../node_modules/expo-application/ios`)
......@@ -1482,8 +1475,6 @@ SPEC REPOS:
- SDWebImageWebPCoder
- Sentry
- SentryPrivate
- SPAlert
- SPIndicator
- UIImageColors
- ZXingObjC
......@@ -1492,8 +1483,6 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/@amplitude/analytics-react-native"
boost:
:podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec"
Burnt:
:path: "../../../node_modules/burnt/ios"
DoubleConversion:
:podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
EthersRS:
......@@ -1677,7 +1666,6 @@ SPEC CHECKSUMS:
Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b
boost: 0a937fbcfdd646fca221c4f1d9750d7ccfdfc2dc
BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33
Burnt: 708556f6283e1b81767e6642e088819d85d1ea08
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
EthersRS: 56b70e73d22d4e894b7e762eef1129159bcd3135
EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903
......@@ -1790,8 +1778,6 @@ SPEC CHECKSUMS:
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
Sentry: 11776f6a25a128808d793d0d41bb7ad873b5ae4f
SentryPrivate: b3c448eacdabe9eab7679a2e0af609c608f91572
SPAlert: 735da1f16a887e294719217572ce1f936d8c8782
SPIndicator: 93e0a4fb23de51294ac48e874c0f081a5e293e4f
UIImageColors: d2ef3b0877d203cbb06489eeb78ea8b7788caabe
Yoga: 135109c9b8c5d1a8af3a58d21cd4c7aa7f3bf555
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
......
......@@ -2450,7 +2450,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2496,7 +2496,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
......@@ -2542,7 +2542,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
......@@ -2588,7 +2588,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
......@@ -2630,7 +2630,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2673,7 +2673,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
......@@ -2716,7 +2716,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
......@@ -2759,7 +2759,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
......@@ -2795,7 +2795,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -2833,7 +2833,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3003,7 +3003,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -3047,7 +3047,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
......@@ -3143,7 +3143,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3214,7 +3214,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
......@@ -3310,7 +3310,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3381,7 +3381,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21.1;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
......@@ -10,6 +10,8 @@
<string>applinks:app.uniswap.org</string>
<string>applinks:app.corn-staging.com</string>
</array>
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.Uniswap</string>
......
......@@ -78,7 +78,7 @@
"@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.31.0",
"@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "4.0.7",
"@uniswap/sdk-core": "4.1.2",
"@uniswap/v3-sdk": "3.10.2",
"@walletconnect/core": "2.10.1",
"@walletconnect/react-native-compat": "2.10.1",
......@@ -156,6 +156,7 @@
"@babel/runtime": "7.18.9",
"@faker-js/faker": "7.6.0",
"@storybook/react": "7.0.2",
"@tamagui/babel-plugin": "1.89.26",
"@testing-library/react-hooks": "7.0.2",
"@testing-library/react-native": "11.5.0",
"@types/react-native": "0.71.3",
......
......@@ -47,7 +47,7 @@ import { useAsyncData } from 'utilities/src/react/hooks'
import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext'
import { config } from 'wallet/src/config'
import { uniswapUrls } from 'wallet/src/constants/urls'
import { initFirebaseAppCheck } from 'wallet/src/features/appCheck/utils'
import { initFirebaseAppCheck } from 'wallet/src/features/appCheck'
import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks'
import { EXPERIMENT_NAMES, FEATURE_FLAGS } from 'wallet/src/features/experiments/constants'
import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors'
......@@ -125,6 +125,8 @@ function App(): JSX.Element | null {
tier: getStatsigEnvironmentTier(),
},
api: uniswapUrls.statsigProxyUrl,
disableAutoMetricsLogging: true,
disableErrorLogging: true,
},
sdkKey: DUMMY_STATSIG_SDK_KEY,
user: deviceId ? { userID: deviceId } : {},
......
import React, { ErrorInfo, PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { Image, StyleSheet } from 'react-native'
import RNRestart from 'react-native-restart'
import { useAppDispatch } from 'src/app/hooks'
import { Button, Flex, Text } from 'ui/src'
import DeadLuni from 'ui/src/assets/graphics/dead-luni.svg'
import { DEAD_LUNI } from 'ui/src/assets'
import { logger } from 'utilities/src/logger/logger'
import { useAccounts } from 'wallet/src/features/wallet/hooks'
import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice'
......@@ -61,9 +62,15 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
}
return (
<Flex centered fill gap="$spacing16" px="$spacing16" py="$spacing48">
<Flex
centered
fill
backgroundColor="$surface1"
gap="$spacing16"
px="$spacing16"
py="$spacing48">
<Flex centered grow gap="$spacing36">
<DeadLuni />
<Image source={DEAD_LUNI} style={styles.errorImage} />
<Flex centered gap="$spacing8">
<Text variant="subheading1">{t('Uh oh!')}</Text>
<Text variant="body2">{t('Something crashed.')}</Text>
......@@ -81,3 +88,11 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
</Flex>
)
}
const styles = StyleSheet.create({
errorImage: {
height: 150,
resizeMode: 'contain',
width: 150,
},
})
import { PropsWithChildren, useCallback } from 'react'
import { useAppDispatch } from 'src/app/hooks'
import { useAppStackNavigation } from 'src/app/navigation/types'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { Screens } from 'src/screens/Screens'
......@@ -8,11 +9,13 @@ import {
NavigateToSwapFlowArgs,
WalletNavigationProvider,
} from 'wallet/src/contexts/WalletNavigationContext'
import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api'
import { ModalName } from 'wallet/src/telemetry/constants'
export function MobileWalletNavigationProvider({ children }: PropsWithChildren): JSX.Element {
const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens)
const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity)
const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens)
const navigateToBuyOrReceiveWithEmptyWallet = useNavigateToBuyOrReceiveWithEmptyWallet()
const navigateToSwapFlow = useNavigateToSwapFlow()
const navigateToTokenDetails = useNavigateToTokenDetails()
......@@ -20,6 +23,7 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren):
<WalletNavigationProvider
navigateToAccountActivityList={navigateToAccountActivityList}
navigateToAccountTokenList={navigateToAccountTokenList}
navigateToBuyOrReceiveWithEmptyWallet={navigateToBuyOrReceiveWithEmptyWallet}
navigateToSwapFlow={navigateToSwapFlow}
navigateToTokenDetails={navigateToTokenDetails}>
{children}
......@@ -59,3 +63,25 @@ function useNavigateToTokenDetails(): (currencyId: string) => void {
[navigation]
)
}
function useNavigateToBuyOrReceiveWithEmptyWallet(): () => void {
const dispatch = useAppDispatch()
const { data } = useFiatOnRampIpAddressQuery()
const fiatOnRampEligible = Boolean(data?.isBuyAllowed)
return useCallback((): void => {
dispatch(closeModal({ name: ModalName.Send }))
if (fiatOnRampEligible) {
dispatch(openModal({ name: ModalName.FiatOnRamp }))
} else {
dispatch(
openModal({
name: ModalName.WalletConnectScan,
initialState: ScannerModalState.WalletQr,
})
)
}
}, [dispatch, fiatOnRampEligible])
}
......@@ -2,17 +2,18 @@ import React from 'react'
import { AccountSwitcherModal } from 'src/app/modals/AccountSwitcherModal'
import { ExperimentsModal } from 'src/app/modals/ExperimentsModal'
import { ExploreModal } from 'src/app/modals/ExploreModal'
import { FiatOnRampAggregatorModal } from 'src/app/modals/FiatOnRampModalAggregator'
import { SwapModal } from 'src/app/modals/SwapModal'
import { TransferTokenModal } from 'src/app/modals/TransferTokenModal'
import { LazyModalRenderer } from 'src/app/modals/utils'
import { ViewOnlyExplainerModal } from 'src/app/modals/ViewOnlyExplainerModal'
import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal'
import { LazyModalRenderer } from 'src/app/modals/utils'
import { RemoveWalletModal } from 'src/components/RemoveWallet/RemoveWalletModal'
import { RestoreWalletModal } from 'src/components/RestoreWalletModal/RestoreWalletModal'
import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal'
import { WalletConnectModals } from 'src/components/WalletConnect/WalletConnectModals'
import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal'
import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal'
import { LockScreenModal } from 'src/features/authentication/LockScreenModal'
import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal'
import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal'
import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal'
import { ScantasticModal } from 'src/features/scantastic/ScantasticModal'
import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal'
......@@ -23,6 +24,10 @@ import { ModalName } from 'wallet/src/telemetry/constants'
export function AppModals(): JSX.Element {
return (
<>
<LazyModalRenderer name={ModalName.ExchangeTransferModal}>
<ExchangeTransferModal />
</LazyModalRenderer>
<LazyModalRenderer name={ModalName.Experiments}>
<ExperimentsModal />
</LazyModalRenderer>
......
......@@ -269,37 +269,26 @@ exports[`AccountSwitcher renders correctly 1`] = `
</View>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={16}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
testID="copy"
......@@ -518,45 +507,27 @@ exports[`AccountSwitcher renders correctly 1`] = `
ExpoLinearGradient
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"marginTop": 16,
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
......@@ -2,7 +2,7 @@ import { ImpactFeedbackStyle } from 'expo-haptics'
import { memo, useMemo } from 'react'
import { I18nManager } from 'react-native'
import { SharedValue } from 'react-native-reanimated'
import { LineChart, LineChartProvider, TLineChartDataProp } from 'react-native-wagmi-charts'
import { LineChart, LineChartProvider } from 'react-native-wagmi-charts'
import PriceExplorerAnimatedNumber from 'src/components/PriceExplorer/PriceExplorerAnimatedNumber'
import { PriceExplorerError } from 'src/components/PriceExplorer/PriceExplorerError'
import { DatetimeText, RelativeChangeText } from 'src/components/PriceExplorer/Text'
......@@ -109,9 +109,7 @@ export const PriceExplorer = memo(function PriceExplorer({
let content: JSX.Element | null
if (forcePlaceholder) {
content = (
<PriceExplorerPlaceholder loading={forcePlaceholder} numberOfDigits={numberOfDigits} />
)
content = <PriceExplorerPlaceholder />
} else if (convertedPriceHistory?.length) {
content = (
// TODO(MOB-2308): add better loading state
......@@ -119,109 +117,86 @@ export const PriceExplorer = memo(function PriceExplorer({
<PriceExplorerChart
additionalPadding={additionalPadding}
lastPricePoint={lastPricePoint}
loading={loading}
numberOfDigits={numberOfDigits}
priceHistory={convertedPriceHistory}
shouldShowAnimatedDot={shouldShowAnimatedDot}
spot={convertedSpot}
tokenColor={tokenColor}
/>
</Flex>
)
} else {
content = <PriceExplorerPlaceholder loading={loading} numberOfDigits={numberOfDigits} />
content = <PriceExplorerPlaceholder />
}
return (
<Flex overflow="hidden">
{content}
<TimeRangeGroup setDuration={setDuration} />
</Flex>
<LineChartProvider
data={convertedPriceHistory ?? []}
onCurrentIndexChange={invokeImpact[ImpactFeedbackStyle.Light]}>
<Flex gap="$spacing8" overflow="hidden">
<PriceTextSection
loading={loading}
numberOfDigits={numberOfDigits}
relativeChange={convertedSpot?.relativeChange}
spotPrice={convertedSpot?.value.value}
/>
{content}
<TimeRangeGroup setDuration={setDuration} />
</Flex>
</LineChartProvider>
)
})
function PriceExplorerPlaceholder({
loading,
numberOfDigits,
}: {
loading: boolean
numberOfDigits: PriceNumberOfDigits
}): JSX.Element {
function PriceExplorerPlaceholder(): JSX.Element {
return (
<Flex gap="$spacing8">
<PriceTextSection loading={loading} numberOfDigits={numberOfDigits} />
<Flex my="$spacing24">
<Loader.Graph />
</Flex>
<Flex my="$spacing24">
<Loader.Graph />
</Flex>
)
}
function PriceExplorerChart({
priceHistory,
spot,
loading,
tokenColor,
additionalPadding,
shouldShowAnimatedDot,
lastPricePoint,
numberOfDigits,
}: {
priceHistory: TLineChartDataProp
spot?: TokenSpotData
loading: boolean
tokenColor?: string
additionalPadding: number
shouldShowAnimatedDot: boolean
lastPricePoint: number
numberOfDigits: PriceNumberOfDigits
}): JSX.Element {
const { chartHeight, chartWidth } = useChartDimensions()
const isRTL = I18nManager.isRTL
return (
<LineChartProvider
data={priceHistory}
onCurrentIndexChange={invokeImpact[ImpactFeedbackStyle.Light]}>
<Flex gap="$spacing8">
<PriceTextSection
loading={loading}
numberOfDigits={numberOfDigits}
relativeChange={spot?.relativeChange}
spotPrice={spot?.value?.value}
/>
{/* TODO(MOB-2166): remove forced LTR direction + scaleX horizontal flip technique once react-native-wagmi-charts fixes this: https://github.com/coinjar/react-native-wagmi-charts/issues/136 */}
<Flex direction="ltr" my="$spacing24" style={{ transform: [{ scaleX: isRTL ? -1 : 1 }] }}>
<LineChart height={chartHeight} width={chartWidth - additionalPadding} yGutter={20}>
<LineChart.Path color={tokenColor} pathProps={{ isTransitionEnabled: false }}>
{shouldShowAnimatedDot && (
<LineChart.Dot
key={lastPricePoint}
hasPulse
// Sometimes, the pulse dot doesn't appear on the end of
// the chart’s path, but on top of the container instead.
// A little shift backwards seems to solve this problem.
at={lastPricePoint - 0.1}
color={tokenColor}
inactiveColor="transparent"
pulseBehaviour="while-inactive"
pulseDurationMs={2000}
size={5}
/>
)}
</LineChart.Path>
<LineChart.CursorLine color={tokenColor} minDurationMs={150} />
<LineChart.CursorCrosshair
// TODO(MOB-2166): remove forced LTR direction + scaleX horizontal flip technique once react-native-wagmi-charts fixes this: https://github.com/coinjar/react-native-wagmi-charts/issues/136
<Flex direction="ltr" my="$spacing24" style={{ transform: [{ scaleX: isRTL ? -1 : 1 }] }}>
<LineChart height={chartHeight} width={chartWidth - additionalPadding} yGutter={20}>
<LineChart.Path color={tokenColor} pathProps={{ isTransitionEnabled: false }}>
{shouldShowAnimatedDot && (
<LineChart.Dot
key={lastPricePoint}
hasPulse
// Sometimes, the pulse dot doesn't appear on the end of
// the chart’s path, but on top of the container instead.
// A little shift backwards seems to solve this problem.
at={lastPricePoint - 0.1}
color={tokenColor}
minDurationMs={150}
outerSize={CURSOR_SIZE}
size={CURSOR_INNER_SIZE}
onActivated={invokeImpact[ImpactFeedbackStyle.Light]}
onEnded={invokeImpact[ImpactFeedbackStyle.Light]}
inactiveColor="transparent"
pulseBehaviour="while-inactive"
pulseDurationMs={2000}
size={5}
/>
</LineChart>
</Flex>
</Flex>
</LineChartProvider>
)}
</LineChart.Path>
<LineChart.CursorLine color={tokenColor} minDurationMs={150} />
<LineChart.CursorCrosshair
color={tokenColor}
minDurationMs={150}
outerSize={CURSOR_SIZE}
size={CURSOR_INNER_SIZE}
onActivated={invokeImpact[ImpactFeedbackStyle.Light]}
onEnded={invokeImpact[ImpactFeedbackStyle.Light]}
/>
</LineChart>
</Flex>
)
}
......@@ -264,10 +264,10 @@ const Numbers = ({
numberOfDigits.left + numberOfDigits.right + Math.floor((numberOfDigits.left - 1) / 3) + 1,
(index) => (
<Animated.View
key={`$number_${index - (commaIndex - decimalPlace.value)}`}
key={`$number_${index - commaIndex}`}
style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}>
<RollNumber
key={`$number_${index - (commaIndex - decimalPlace.value)}`}
key={`$number_${index - commaIndex}`}
chars={chars}
commaIndex={commaIndex}
currency={currency}
......
import { maxBy } from 'lodash'
import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react'
import { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react'
import { SharedValue } from 'react-native-reanimated'
import { TLineChartData } from 'react-native-wagmi-charts'
import { PollingInterval } from 'wallet/src/constants/misc'
......@@ -42,6 +42,11 @@ export function useTokenPriceHistory(
error: boolean
numberOfDigits: PriceNumberOfDigits
} {
const lastPrice = useRef<undefined | number>(undefined)
const lastNumberOfDigits = useRef({
left: 0,
right: 0,
})
const [duration, setDuration] = useState(initialDuration)
const { convertFiatAmount } = useLocalizationContext()
......@@ -64,14 +69,15 @@ export function useTokenPriceHistory(
const offChainData = priceData?.tokenProjects?.[0]?.markets?.[0]
const onChainData = priceData?.tokenProjects?.[0]?.tokens?.[0]?.market
const price = offChainData?.price?.value ?? onChainData?.price?.value
const price = offChainData?.price?.value ?? onChainData?.price?.value ?? lastPrice.current
lastPrice.current = price
const priceHistory = offChainData?.priceHistory ?? onChainData?.priceHistory
const pricePercentChange24h =
offChainData?.pricePercentChange24h?.value ?? onChainData?.pricePercentChange24h?.value ?? 0
const spot = useMemo(
() =>
price
price !== undefined
? {
value: { value: price },
relativeChange: { value: pricePercentChange24h },
......@@ -93,16 +99,15 @@ export function useTokenPriceHistory(
const convertedMaxValue = convertFiatAmount(max?.value).amount
if (max) {
return {
const newNumberOfDigits = {
left: String(convertedMaxValue).split('.')[0]?.length || 10,
right: Number(String(convertedMaxValue.toFixed(10)).split('.')[0]) > 0 ? 2 : 10,
}
lastNumberOfDigits.current = newNumberOfDigits
return newNumberOfDigits
}
return {
left: 0,
right: 0,
}
return lastNumberOfDigits.current
}, [convertFiatAmount, priceHistory])
const retry = useCallback(async () => {
......
import { impactAsync, ImpactFeedbackStyle, selectionAsync } from 'expo-haptics'
import React, { useCallback } from 'react'
import React, { useCallback, useEffect } from 'react'
import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { navigate } from 'src/app/navigation/rootNavigation'
import { openModal } from 'src/features/modals/modalSlice'
import { setUserProperty } from 'src/features/telemetry'
import { UserPropertyName } from 'src/features/telemetry/constants'
import { Screens } from 'src/screens/Screens'
import { isDevBuild } from 'src/utils/version'
import { Flex, Icons, Text, TouchableArea } from 'ui/src'
......@@ -29,6 +31,20 @@ export function AccountHeader(): JSX.Element {
const { avatar } = useAvatar(activeAddress)
const displayName = useDisplayName(activeAddress)
// Log ENS and Unitag ownership for user usage stats
useEffect(() => {
switch (displayName?.type) {
case DisplayNameType.ENS:
setUserProperty(UserPropertyName.HasLoadedENS, true)
return
case DisplayNameType.Unitag:
setUserProperty(UserPropertyName.HasLoadedUnitag, true)
return
default:
return
}
}, [displayName?.type])
const onPressAccountHeader = useCallback(() => {
dispatch(openModal({ name: ModalName.AccountSwitcher }))
}, [dispatch])
......
......@@ -33,39 +33,28 @@ exports[`AccountHeader renders without error 1`] = `
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={20}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onLongPress={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"alignItems": "center",
"flexDirection": "row",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
testID="manage"
......@@ -334,37 +323,26 @@ exports[`AccountHeader renders without error 1`] = `
</View>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={20}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
>
......@@ -440,38 +418,27 @@ exports[`AccountHeader renders without error 1`] = `
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={20}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"flexShrink": 1,
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
>
......@@ -565,38 +532,27 @@ exports[`AccountHeader renders without error 1`] = `
</Text>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={20}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"paddingLeft": 8,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
......@@ -108,48 +108,31 @@ exports[`AccountList renders without error 1`] = `
onPress={[Function]}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onLongPress={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"paddingBottom": 12,
"paddingLeft": 24,
"paddingRight": 24,
"paddingTop": 8,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
......@@ -42,7 +42,7 @@ export function CopyTextButton({ copyText }: Props): JSX.Element {
return (
<Button icon={isCopied ? copiedIcon : copyIcon} theme="tertiary" onPress={onPress}>
{isCopied ? t`Copied!` : t`Copy`}
{isCopied ? t`Copied` : t`Copy`}
</Button>
)
}
......@@ -264,7 +264,7 @@ function FavoritesSection(props: FavoritesSectionProps): JSX.Element | null {
px="$spacing12"
zIndex={1}>
{hasFavoritedTokens && <FavoriteTokensGrid {...props} />}
{hasFavoritedWallets && <FavoriteWalletsGrid showLoading={props.showLoading} />}
{hasFavoritedWallets && <FavoriteWalletsGrid {...props} />}
</Flex>
)
}
......
......@@ -2,18 +2,11 @@ import { ImpactFeedbackStyle } from 'expo-haptics'
import React, { memo, useCallback } from 'react'
import { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view'
import {
FadeIn,
SharedValue,
interpolate,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
import { FadeIn, SharedValue } from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import RemoveButton from 'src/components/explore/RemoveButton'
import { useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { Loader } from 'src/components/loading'
import { disableOnPress } from 'src/utils/disableOnPress'
import { usePollOnFocusOnly } from 'src/utils/hooks'
......@@ -55,8 +48,6 @@ function FavoriteTokenCard({
const dispatch = useAppDispatch()
const tokenDetailsNavigation = useTokenDetailsNavigation()
const { convertFiatAmountFormatted } = useLocalizationContext()
const dragAnimationProgress = useSharedValue(0)
const wasTouched = useSharedValue(false)
const { data, networkStatus, startPolling, stopPolling } = useFavoriteTokenCardQuery({
variables: currencyIdToContractInput(currencyId),
......@@ -103,45 +94,14 @@ function FavoriteTokenCard({
tokenDetailsNavigation.navigate(currencyId)
}
useAnimatedReaction(
() => dragActivationProgress.value,
(activationProgress, prev) => {
const prevActivationProgress = prev ?? 0
// If the activation progress is increasing (the user is touching one of the cards)
if (activationProgress > prevActivationProgress) {
if (isTouched.value) {
// If the current card is the one being touched, reset the animation progress
wasTouched.value = true
dragAnimationProgress.value = 0
} else {
// Otherwise, animate the card
wasTouched.value = false
dragAnimationProgress.value = activationProgress
}
}
// If the activation progress is decreasing (the user is no longer touching one of the cards)
else {
if (isTouched.value || wasTouched.value) {
// If the current card is the one that was being touched, reset the animation progress
dragAnimationProgress.value = 0
} else {
// Otherwise, animate the card
dragAnimationProgress.value = activationProgress
}
}
}
)
const animatedStyle = useAnimatedStyle(() => ({
opacity: interpolate(dragAnimationProgress.value, [0, 1], [1, 0.5]),
}))
const animatedDragStyle = useAnimatedCardDragStyle(isTouched, dragActivationProgress)
if (isNonPollingRequestInFlight(networkStatus)) {
return <Loader.Favorite height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
}
return (
<AnimatedFlex style={animatedStyle}>
<AnimatedFlex style={animatedDragStyle}>
<ContextMenu
actions={menuActions}
disabled={isEditing}
......
import { ImpactFeedbackStyle } from 'expo-haptics'
import { default as React, useCallback, useMemo } from 'react'
import { memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view'
import { SharedValue } from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks'
import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks'
import { useAnimatedCardDragStyle } from 'src/components/explore/hooks'
import RemoveButton from 'src/components/explore/RemoveButton'
import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, TouchableArea } from 'ui/src'
import { AnimatedFlex, Flex, TouchableArea } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
......@@ -19,12 +21,16 @@ import { DisplayNameType } from 'wallet/src/features/wallet/types'
type FavoriteWalletCardProps = {
address: Address
isEditing?: boolean
isTouched: SharedValue<boolean>
dragActivationProgress: SharedValue<number>
setIsEditing: (update: boolean) => void
} & ViewProps
export default function FavoriteWalletCard({
function FavoriteWalletCard({
address,
isEditing,
isTouched,
dragActivationProgress,
setIsEditing,
...rest
}: FavoriteWalletCardProps): JSX.Element {
......@@ -51,51 +57,60 @@ export default function FavoriteWalletCard({
]
}, [t])
const animatedDragStyle = useAnimatedCardDragStyle(isTouched, dragActivationProgress)
return (
<ContextMenu
actions={menuActions}
disabled={isEditing}
style={{ borderRadius: borderRadii.rounded16 }}
onPress={(e): void => {
// Emitted index based on order of menu action array
// remove favorite action
if (e.nativeEvent.index === 0) {
onRemove()
}
// Edit mode toggle action
if (e.nativeEvent.index === 1) {
setIsEditing(true)
}
}}
{...rest}>
<TouchableArea
hapticFeedback
borderRadius="$rounded16"
hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4"
onLongPress={disableOnPress}
onPress={(): void => {
navigate(address)
<AnimatedFlex style={animatedDragStyle}>
<ContextMenu
actions={menuActions}
disabled={isEditing}
style={{ borderRadius: borderRadii.rounded16 }}
onPress={(e): void => {
// Emitted index based on order of menu action array
// remove favorite action
if (e.nativeEvent.index === 0) {
onRemove()
}
// Edit mode toggle action
if (e.nativeEvent.index === 1) {
setIsEditing(true)
}
}}
onPressIn={async (): Promise<void> => {
await preload(address)
}}>
<BaseCard.Shadow>
<Flex row gap="$spacing4" justifyContent="space-between">
<Flex row shrink alignItems="center" gap="$spacing8">
{icon}
<DisplayNameText
displayName={displayName}
textProps={{
adjustsFontSizeToFit: displayName?.type === DisplayNameType.Address,
variant: 'body1',
}}
/>
{...rest}>
<TouchableArea
hapticFeedback
activeOpacity={isEditing ? 1 : undefined}
backgroundColor="$surface2"
borderRadius="$rounded16"
disabled={isEditing}
hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4"
onLongPress={disableOnPress}
onPress={(): void => {
navigate(address)
}}
onPressIn={async (): Promise<void> => {
await preload(address)
}}>
<BaseCard.Shadow>
<Flex row gap="$spacing4" justifyContent="space-between">
<Flex row shrink alignItems="center" gap="$spacing8">
{icon}
<DisplayNameText
displayName={displayName}
textProps={{
adjustsFontSizeToFit: displayName?.type === DisplayNameType.Address,
variant: 'body1',
}}
/>
</Flex>
<RemoveButton visible={isEditing} onPress={onRemove} />
</Flex>
<RemoveButton visible={isEditing} onPress={onRemove} />
</Flex>
</BaseCard.Shadow>
</TouchableArea>
</ContextMenu>
</BaseCard.Shadow>
</TouchableArea>
</ContextMenu>
</AnimatedFlex>
)
}
export default memo(FavoriteWalletCard)
import { default as React, useEffect, useMemo, useState } from 'react'
import { default as React, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FadeIn } from 'react-native-reanimated'
import { FadeIn, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import { useAppSelector } from 'src/app/hooks'
import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow'
import FavoriteWalletCard from 'src/components/explore/FavoriteWalletCard'
import { Loader } from 'src/components/loading'
import {
AutoScrollProps,
SortableGrid,
SortableGridChangeEvent,
SortableGridRenderItem,
} from 'src/components/sortableGrid'
import { AnimatedFlex, Flex } from 'ui/src'
import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors'
import { setFavoriteWallets } from 'wallet/src/features/favorites/slice'
import { useAppDispatch } from 'wallet/src/state'
const NUM_COLUMNS = 2
const ITEM_FLEX = { flex: 1 / NUM_COLUMNS }
const HALF_WIDTH = { width: '50%' }
type FavoriteWalletsGridProps = AutoScrollProps & {
showLoading: boolean
}
/** Renders the favorite wallets section on the Explore tab */
export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }): JSX.Element {
export function FavoriteWalletsGrid({
showLoading,
...rest
}: FavoriteWalletsGridProps): JSX.Element {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const [isEditing, setIsEditing] = useState(false)
const isTokenDragged = useSharedValue(false)
const watchedWalletsSet = useAppSelector(selectWatchedAddressSet)
const watchedWalletsList = useMemo(() => Array.from(watchedWalletsSet), [watchedWalletsSet])
......@@ -27,8 +43,33 @@ export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }):
}
}, [watchedWalletsSet.size])
const handleOrderChange = useCallback(
({ data }: SortableGridChangeEvent<string>) => {
dispatch(setFavoriteWallets({ addresses: data }))
},
[dispatch]
)
const renderItem = useCallback<SortableGridRenderItem<string>>(
({ item: address, isTouched, dragActivationProgress }): JSX.Element => (
<FavoriteWalletCard
key={address}
address={address}
dragActivationProgress={dragActivationProgress}
isEditing={isEditing}
isTouched={isTouched}
setIsEditing={setIsEditing}
/>
),
[isEditing]
)
const animatedStyle = useAnimatedStyle(() => ({
zIndex: isTokenDragged.value ? 1 : 0,
}))
return (
<AnimatedFlex entering={FadeIn}>
<AnimatedFlex entering={FadeIn} style={animatedStyle}>
<FavoriteHeaderRow
editingTitle={t('Edit favorite wallets')}
isEditing={isEditing}
......@@ -38,17 +79,21 @@ export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }):
{showLoading ? (
<FavoriteWalletsGridLoader />
) : (
<Flex row flexWrap="wrap">
{watchedWalletsList.map((address) => (
<FavoriteWalletCard
key={address}
address={address}
isEditing={isEditing}
setIsEditing={setIsEditing}
style={HALF_WIDTH}
/>
))}
</Flex>
<SortableGrid
{...rest}
activeItemOpacity={1}
data={watchedWalletsList}
editable={isEditing}
numColumns={NUM_COLUMNS}
renderItem={renderItem}
onChange={handleOrderChange}
onDragEnd={(): void => {
isTokenDragged.value = false
}}
onDragStart={(): void => {
isTokenDragged.value = true
}}
/>
)}
</AnimatedFlex>
)
......
import { SharedEventName } from '@uniswap/analytics-events'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { NativeSyntheticEvent, Share } from 'react-native'
import { NativeSyntheticEvent, Share, ViewStyle } from 'react-native'
import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view'
import {
AnimateStyle,
SharedValue,
interpolate,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/features/favorites/hooks'
import { openModal } from 'src/features/modals/modalSlice'
......@@ -148,3 +156,44 @@ export function useExploreTokenContextMenu({
return { menuActions, onContextMenuPress }
}
export function useAnimatedCardDragStyle(
isTouched: SharedValue<boolean>,
dragActivationProgress: SharedValue<number>
): AnimateStyle<ViewStyle> {
const wasTouched = useSharedValue(false)
const dragAnimationProgress = useSharedValue(0)
useAnimatedReaction(
() => dragActivationProgress.value,
(activationProgress, prev) => {
const prevActivationProgress = prev ?? 0
// If the activation progress is increasing (the user is touching one of the cards)
if (activationProgress > prevActivationProgress) {
if (isTouched.value) {
// If the current card is the one being touched, reset the animation progress
wasTouched.value = true
dragAnimationProgress.value = 0
} else {
// Otherwise, animate the card
wasTouched.value = false
dragAnimationProgress.value = activationProgress
}
}
// If the activation progress is decreasing (the user is no longer touching one of the cards)
else {
if (isTouched.value || wasTouched.value) {
// If the current card is the one that was being touched, reset the animation progress
dragAnimationProgress.value = 0
} else {
// Otherwise, animate the card
dragAnimationProgress.value = activationProgress
}
}
}
)
return useAnimatedStyle(() => ({
opacity: interpolate(dragAnimationProgress.value, [0, 1], [1, 0.5]),
}))
}
......@@ -468,44 +468,27 @@ exports[`SearchPopularTokens renders without error 2`] = `
onPress={[Function]}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onLongPress={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
testID="search-token-item"
......@@ -667,44 +650,27 @@ exports[`SearchPopularTokens renders without error 2`] = `
onPress={[Function]}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onLongPress={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
testID="search-token-item"
......
......@@ -2,17 +2,23 @@ import { Currency } from '@uniswap/sdk-core'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import { useFiatOnRampLogoUrl } from 'src/components/fiatOnRamp/hooks'
import { Loader } from 'src/components/loading'
import { useFormatExactCurrencyAmount } from 'src/features/fiatOnRamp/hooks'
import { Flex, Icons, Text, TouchableArea } from 'ui/src'
import { Flex, Icons, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { fonts, iconSizes } from 'ui/src/theme'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types'
import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils'
import { ImageUri } from 'wallet/src/features/images/ImageUri'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
function LogoLoader(): JSX.Element {
return (
<Loader.Box borderRadius="$roundedFull" height={iconSizes.icon40} width={iconSizes.icon40} />
)
}
export function FORQuoteItem({
quote,
serviceProvider,
......@@ -46,7 +52,8 @@ export function FORQuoteItem({
currencySymbol: baseCurrency.symbol,
})
const logoUrl = useFiatOnRampLogoUrl(serviceProvider?.logos)
const isDarkMode = useIsDarkMode()
const logoUrl = getServiceProviderLogo(serviceProvider?.logos, isDarkMode)
return (
<TouchableArea onPress={onPress}>
......@@ -62,11 +69,17 @@ export function FORQuoteItem({
<QuoteLoader showCarret={showCarret} />
) : (
<Flex row alignItems="center" gap="$spacing12">
<Loader.Box
borderRadius="$roundedFull"
height={iconSizes.icon40}
width={iconSizes.icon40}
/>
<Flex>
{logoUrl ? (
<ImageUri
fallback={<LogoLoader />}
imageStyle={ServiceProviderLogoStyles.icon}
uri={logoUrl}
/>
) : (
<LogoLoader />
)}
</Flex>
<Flex shrink gap="$spacing4">
<Text color="$neutral1" variant="subheading2">
{serviceProvider?.name}
......@@ -96,23 +109,6 @@ export function FORQuoteItem({
<Flex />
)}
</Flex>
{
// TODO: Enable once https://linear.app/uniswap/issue/MOB-2565/implement-service-providers-logo-once-meld-has-added-them-on-their is unblocked
false && logoUrl && (
<ImageUri
fallback={
<Loader.Box
borderRadius="$roundedFull"
height={iconSizes.icon40}
width={iconSizes.icon40}
/>
}
imageStyle={ServiceProviderLogoStyles.icon}
resizeMode="contain"
uri={logoUrl}
/>
)
}
</Flex>
)}
</Flex>
......
import { useIsDarkMode } from 'ui/src'
import { FORLogo } from 'wallet/src/features/fiatOnRamp/types'
export function useFiatOnRampLogoUrl(logos: FORLogo | undefined): string | undefined {
const isDarkMode = useIsDarkMode()
if (!logos) {
return
}
return isDarkMode ? logos.darkLogo : logos.lightLogo
}
import React from 'react'
import { WalletLoader } from 'src/components/loading/WalletLoader'
import { render } from 'src/test/test-utils'
it('renders wallet loader', () => {
const tree = render(<WalletLoader opacity={1} />)
expect(tree).toMatchSnapshot()
})
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders wallet loader 1`] = `
<View
sentry-label="WalletLoader"
style={
{
"alignItems": "center",
"borderBottomColor": "#CECECE",
"borderBottomLeftRadius": 20,
"borderBottomRightRadius": 20,
"borderBottomWidth": 1,
"borderLeftColor": "#CECECE",
"borderLeftWidth": 1,
"borderRightColor": "#CECECE",
"borderRightWidth": 1,
"borderStyle": "solid",
"borderTopColor": "#CECECE",
"borderTopLeftRadius": 20,
"borderTopRightRadius": 20,
"borderTopWidth": 1,
"flexDirection": "row",
"justifyContent": "flex-start",
"opacity": 1,
"overflow": "hidden",
"paddingBottom": 16,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 16,
}
}
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"gap": 12,
"height": 36,
}
}
>
<View
style={
{
"backgroundColor": "#CECECE",
"borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999,
"borderTopLeftRadius": 999999,
"borderTopRightRadius": 999999,
"flexDirection": "column",
"height": 32,
"width": 32,
}
}
/>
<View
style={
{
"alignItems": "flex-start",
"flexDirection": "column",
"width": "100%",
}
}
>
<View
onLayout={[Function]}
style={
{
"flexDirection": "column",
"opacity": 0,
}
}
testID="shimmer-placeholder"
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
}
}
testID="text-placeholder"
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"position": "relative",
}
}
>
<View
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
style={
{
"color": "transparent",
"fontFamily": "Basel-Book",
"fontSize": 19,
"lineHeight": 24,
"opacity": 0,
}
}
suppressHighlighting={true}
>
Wallet Nickname
</Text>
</View>
<View
style={
{
"backgroundColor": "#F9F9F9",
"borderBottomLeftRadius": 4,
"borderBottomRightRadius": 4,
"borderTopLeftRadius": 4,
"borderTopRightRadius": 4,
"bottom": "5%",
"flexDirection": "column",
"left": 0,
"position": "absolute",
"right": 0,
"top": "5%",
}
}
/>
</View>
</View>
</View>
<View
onLayout={[Function]}
style={
{
"flexDirection": "column",
"opacity": 0,
}
}
testID="shimmer-placeholder"
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
}
}
testID="text-placeholder"
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"position": "relative",
}
}
>
<View
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
style={
{
"color": "transparent",
"fontFamily": "Basel-Book",
"fontSize": 17,
"lineHeight": 24,
"opacity": 0,
}
}
suppressHighlighting={true}
>
0xaaaa...aaaa
</Text>
</View>
<View
style={
{
"backgroundColor": "#F9F9F9",
"borderBottomLeftRadius": 4,
"borderBottomRightRadius": 4,
"borderTopLeftRadius": 4,
"borderTopRightRadius": 4,
"bottom": "5%",
"flexDirection": "column",
"left": 0,
"position": "absolute",
"right": 0,
"top": "5%",
}
}
/>
</View>
</View>
</View>
</View>
</View>
</View>
`;
import React, { memo } from 'react'
import { TransactionLoader } from 'src/components/loading/TransactionLoader'
import { WalletLoader } from 'src/components/loading/WalletLoader'
import { WaveLoader } from 'src/components/loading/WaveLoader'
import { Flex, FlexLoader, FlexLoaderProps, getToken, Skeleton } from 'ui/src'
......@@ -12,20 +11,6 @@ function Graph(): JSX.Element {
)
}
function Wallets({ repeat = 1 }: { repeat?: number }): JSX.Element {
return (
<Skeleton>
<Flex gap="$spacing12">
{new Array(repeat).fill(null).map((_, i, { length }) => (
<React.Fragment key={i}>
<WalletLoader opacity={(length - i) / length} />
</React.Fragment>
))}
</Flex>
</Skeleton>
)
}
export const Transaction = memo(function _Transaction({
repeat = 1,
}: {
......@@ -72,7 +57,6 @@ function Favorite({ height, contrast }: { height?: number; contrast?: boolean })
export const Loader = {
Box,
Transaction,
Wallets,
Graph,
Image,
Favorite,
......
......@@ -30,6 +30,9 @@ export function useAnimatedZIndex(renderIndex: number): SharedValue<number> {
previousActiveIndex: previousActiveIndexValue.value,
}),
({ touchedIndex, previousActiveIndex }) => {
if (touchedIndex === null) {
return null
}
if (renderIndex === touchedIndex) {
// Display the currently touched item on top of all other items
zIndexValue.value = 10000
......
......@@ -49,15 +49,15 @@ export function UnitagsIntroModal(): JSX.Element {
<Flex gap="$spacing24" px="$spacing24" py="$spacing16">
<Flex alignItems="center" gap="$spacing12">
<Text variant="subheading1">{t('Introducing usernames')}</Text>
<Text color="$neutral2" textAlign="center" variant="body3">
<Text color="$neutral2" textAlign="center" variant="body2">
{t(
'Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.'
)}
</Text>
</Flex>
<Flex alignItems="center" maxHeight={100}>
<Flex alignItems="center" maxHeight={105}>
<Image
maxHeight={100}
maxHeight={105}
resizeMode="contain"
source={isDarkMode ? UNITAGS_INTRO_BANNER_DARK : UNITAGS_INTRO_BANNER_LIGHT}
/>
......
import React, { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { Keyboard, TextInput } from 'react-native'
import { PasswordInput } from 'src/components/input/PasswordInput'
import { PasswordError } from 'src/features/onboarding/PasswordError'
import { Button, Flex, Icons, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { useDebounce } from 'utilities/src/time/timing'
import { ElementName } from 'wallet/src/telemetry/constants'
import { validatePassword } from 'wallet/src/utils/password'
import {
PASSWORD_VALIDATION_DEBOUNCE_MS,
PasswordStrength,
getPasswordStrength,
getPasswordStrengthTextAndColor,
isPasswordStrongEnough,
} from 'wallet/src/utils/password'
export enum PasswordErrors {
WeakPassword = 'WeakPassword',
......@@ -29,9 +36,17 @@ export function CloudBackupPasswordForm({
const passwordInputRef = useRef<TextInput>(null)
const [password, setPassword] = useState('')
const [error, setError] = useState<PasswordErrors | string | undefined>(undefined)
const [error, setError] = useState<PasswordErrors | undefined>(undefined)
const isButtonDisabled = !!error || password.length === 0
const [passwordStrength, setPasswordStrength] = useState(PasswordStrength.NONE)
const debouncedPasswordStrength = useDebounce(passwordStrength, PASSWORD_VALIDATION_DEBOUNCE_MS)
const isStrongPassword = isPasswordStrongEnough({
minStrength: PasswordStrength.MEDIUM,
currentStrength: passwordStrength,
})
const isButtonDisabled =
!!error || password.length === 0 || (!isConfirmation && !isStrongPassword)
const onPasswordChangeText = (newPassword: string): void => {
if (isConfirmation && newPassword === password) {
......@@ -39,15 +54,15 @@ export function CloudBackupPasswordForm({
}
// always reset error if not confirmation
if (!isConfirmation) {
setPasswordStrength(getPasswordStrength(newPassword))
setError(undefined)
}
setPassword(newPassword)
}
const onPasswordSubmitEditing = (): void => {
const { valid, validationErrorString } = validatePassword(password)
if (!isConfirmation && !valid) {
setError(validationErrorString || PasswordErrors.WeakPassword)
if (!isConfirmation && !isStrongPassword) {
setError(PasswordErrors.WeakPassword)
return
}
if (isConfirmation && passwordToConfirm !== password) {
......@@ -59,9 +74,8 @@ export function CloudBackupPasswordForm({
}
const onPressNext = (): void => {
const { valid, validationErrorString } = validatePassword(password)
if (!isConfirmation && !valid) {
setError(validationErrorString || PasswordErrors.WeakPassword)
if (!isConfirmation && !isStrongPassword) {
setError(PasswordErrors.WeakPassword)
return
}
if (isConfirmation && passwordToConfirm !== password) {
......@@ -99,6 +113,7 @@ export function CloudBackupPasswordForm({
}}
onSubmitEditing={onPasswordSubmitEditing}
/>
{!isConfirmation && <PasswordStrengthText strength={debouncedPasswordStrength} />}
{error ? <PasswordError errorText={errorText} /> : null}
</Flex>
{!isConfirmation && (
......@@ -118,3 +133,17 @@ export function CloudBackupPasswordForm({
</>
)
}
function PasswordStrengthText({ strength }: { strength: PasswordStrength }): JSX.Element {
const { text, color } = getPasswordStrengthTextAndColor(strength)
const hasPassword = strength !== PasswordStrength.NONE
return (
<Flex centered row opacity={hasPassword ? 1 : 0} pt="$spacing12" px="$spacing8">
<Text color={color} variant="body3">
<Trans>This is a {text.toLowerCase()} password</Trans>
</Text>
</Flex>
)
}
......@@ -45,6 +45,13 @@ interface ProfileHeaderProps {
address: Address
}
const HEADER_SOLID_COLOR_OPACITY = 0.1
export const solidHeaderProps = {
minOpacity: HEADER_SOLID_COLOR_OPACITY,
maxOpacity: HEADER_SOLID_COLOR_OPACITY,
}
export const ProfileHeader = memo(function ProfileHeader({
address,
}: ProfileHeaderProps): JSX.Element {
......@@ -167,9 +174,12 @@ export const ProfileHeader = memo(function ProfileHeader({
/>
</Flex>
{hasAvatar && avatarColors?.primary ? (
<HeaderRadial color={avatarColors.primary} />
<HeaderRadial color={avatarColors.primary} {...solidHeaderProps} />
) : (
<HeaderRadial color={isUniconsV2Enabled ? color : uniconGradientStart} />
<HeaderRadial
color={isUniconsV2Enabled ? color : uniconGradientStart}
{...solidHeaderProps}
/>
)}
</AnimatedFlex>
......
import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState'
import { ExchangeTransferConnecting } from 'src/screens/ExchangeTransferConnecting'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { ModalName } from 'wallet/src/telemetry/constants'
export function ExchangeTransferModal(): JSX.Element | null {
const dispatch = useAppDispatch()
const onClose = (): void => {
dispatch(closeModal({ name: ModalName.ExchangeTransferModal }))
}
const { initialState } = useAppSelector(selectModalState(ModalName.ExchangeTransferModal))
const serviceProvider = initialState?.serviceProvider
return serviceProvider ? (
<BottomSheetModal
fullScreen
hideHandlebar
hideKeyboardOnDismiss
renderBehindTopInset
name={ModalName.ExchangeTransferModal}
onClose={onClose}>
<ExchangeTransferConnecting serviceProvider={serviceProvider} onClose={onClose} />
</BottomSheetModal>
) : null
}
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
export interface ExchangeTransferModalState {
serviceProvider: FORTransferInstitution
}
......@@ -11,6 +11,7 @@ import {
import { iconSizes } from 'ui/src/theme'
export const SERVICE_PROVIDER_ICON_SIZE = 90
export const SERVICE_PROVIDER_ICON_BORDER_RADIUS = 20
export function FiatOnRampConnectingView({
amount,
......@@ -18,7 +19,7 @@ export function FiatOnRampConnectingView({
serviceProviderName,
serviceProviderLogo,
}: {
amount: string
amount?: string
quoteCurrencyCode?: string
serviceProviderName: string
serviceProviderLogo?: JSX.Element
......@@ -48,7 +49,7 @@ export function FiatOnRampConnectingView({
<Text variant="subheading1">
{t('Connecting you to {{serviceProvider}}', { serviceProvider: serviceProviderName })}
</Text>
{quoteCurrencyCode && (
{quoteCurrencyCode && amount && (
<Text color="$neutral2" variant="body2">
{t('Buying {{amount}} worth of {{quoteCurrencyCode}}', {
amount,
......@@ -73,7 +74,7 @@ const styles = StyleSheet.create({
},
uniswapLogoWrapper: {
backgroundColor: '#FFEFF8', // #FFD8EF with 40% opacity on a white background
borderRadius: 20,
borderRadius: SERVICE_PROVIDER_ICON_BORDER_RADIUS,
height: SERVICE_PROVIDER_ICON_SIZE,
width: SERVICE_PROVIDER_ICON_SIZE,
},
......
import { BottomSheetFlatList } from '@gorhom/bottom-sheet'
import { ImpactFeedbackStyle } from 'expo-haptics'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { ListRenderItemInfo } from 'react-native'
import { getCountry } from 'react-native-localize'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks'
import { openModal } from 'src/features/modals/modalSlice'
import { AnimatedFlex, Flex, Loader, Text, TouchableArea } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { useFiatOnRampAggregatorTransferInstitutionsQuery } from 'wallet/src/features/fiatOnRamp/api'
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
import { RemoteImage } from 'wallet/src/features/images/RemoteImage'
import { ModalName } from 'wallet/src/telemetry/constants'
function key(item: FORTransferInstitution): string {
return item.id as string
......@@ -25,7 +27,6 @@ function CEXItemWrapper({
institution: FORTransferInstitution
onSelectTransferInstitution: (transferInstitution: FORTransferInstitution) => void
}): JSX.Element | null {
const { t } = useTranslation()
const onPress = (): void => onSelectTransferInstitution(institution)
return (
......@@ -53,23 +54,29 @@ function CEXItemWrapper({
{institution.name}
</Text>
</Flex>
<Text color="$neutral3" variant="body3">
{t('Not linked')}
</Text>
</Flex>
</TouchableArea>
)
}
export function TransferInstitutionSelector(): JSX.Element {
export function TransferInstitutionSelector({ onClose }: { onClose: () => void }): JSX.Element {
const dispatch = useAppDispatch()
const { data, isLoading } = useFiatOnRampAggregatorTransferInstitutionsQuery({
countryCode: getCountry(),
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onSelectTransferInstitution = useCallback((transferInstitution: FORTransferInstitution) => {
//TODO(MOB-2603): fetch widget and launch transfer flow
}, [])
const onSelectTransferInstitution = useCallback(
(transferInstitution: FORTransferInstitution) => {
dispatch(
openModal({
name: ModalName.ExchangeTransferModal,
initialState: { serviceProvider: transferInstitution },
})
)
onClose()
},
[dispatch, onClose]
)
const renderItem = useCallback(
({ item: institution }: ListRenderItemInfo<FORTransferInstitution>) => (
......
......@@ -19,6 +19,7 @@ import {
isInvalidRequestAmountTooLow,
} from 'wallet/src/features/fiatOnRamp/utils'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
// TODO: https://linear.app/uniswap/issue/MOB-2532/implement-fetching-of-available-fiat-currencies-from-meld
const MELD_FIAT_CURRENCY_CODES = ['usd', 'eur']
......@@ -60,6 +61,7 @@ export function useFiatOnRampQuotes({
quotes: FORQuote[] | undefined
} {
const debouncedBaseCurrencyAmount = useDebounce(baseCurrencyAmount, Delay.Short)
const walletAddress = useActiveAccountAddress()
const {
currentData: quotesResponse,
......@@ -72,6 +74,7 @@ export function useFiatOnRampQuotes({
sourceCurrencyCode: baseCurrencyCode,
destinationCurrencyCode: quoteCurrencyCode,
countryCode,
walletAddress: walletAddress ?? '',
}
: skipToken,
{
......
......@@ -24,6 +24,7 @@ import { FORSupportedToken, MoonpayCurrency } from 'wallet/src/features/fiatOnRa
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { addTransaction } from 'wallet/src/features/transactions/slice'
import {
FiatPurchaseTransactionInfo,
TransactionDetails,
TransactionStatus,
TransactionType,
......@@ -58,7 +59,10 @@ export function useFormatExactCurrencyAmount(
}
/** Returns a new externalTransactionId and a callback to store the transaction. */
export function useFiatOnRampTransactionCreator(ownerAddress: string): {
export function useFiatOnRampTransactionCreator(
ownerAddress: string,
initialTypeInfo?: Partial<FiatPurchaseTransactionInfo>
): {
externalTransactionId: string
dispatchAddTransaction: () => void
} {
......@@ -73,7 +77,11 @@ export function useFiatOnRampTransactionCreator(ownerAddress: string): {
chainId: ChainId.Mainnet,
id: externalTransactionId.current,
from: ownerAddress,
typeInfo: { type: TransactionType.FiatPurchase, syncedWithBackend: false },
typeInfo: {
...initialTypeInfo,
type: TransactionType.FiatPurchase,
syncedWithBackend: false,
},
status: TransactionStatus.Pending,
addedTime: Date.now(),
hash: '',
......@@ -81,7 +89,7 @@ export function useFiatOnRampTransactionCreator(ownerAddress: string): {
}
// use addTransaction action so transactionWatcher picks it up
dispatch(addTransaction(transactionDetail))
}, [dispatch, externalTransactionId, ownerAddress])
}, [dispatch, ownerAddress, initialTypeInfo])
return { externalTransactionId: externalTransactionId.current, dispatchAddTransaction }
}
......
......@@ -120,7 +120,7 @@ function Inputs({
py="$none"
returnKeyType="done"
scrollEnabled={false}
selectionColor={colors.neutral1.get()}
selectionColor={colors.neutral1.val}
spellCheck={false}
testID="import_account_form/input"
textAlign={isInputEmpty ? 'left' : backgroundTextAlignment}
......
......@@ -158,41 +158,17 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"backgroundColor": "#FFEFFF",
......@@ -200,12 +176,17 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"onPressIn": undefined,
"flexDirection": "column",
"opacity": 1,
"paddingBottom": 8,
"paddingLeft": 8,
"paddingRight": 8,
"paddingTop": 8,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
......@@ -3,6 +3,7 @@ import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { Screens } from 'src/screens/Screens'
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
import { TransactionState } from 'wallet/src/features/transactions/transactionState/types'
import { ModalName } from 'wallet/src/telemetry/constants'
......@@ -13,6 +14,9 @@ export interface AppModalState<T> {
export interface ModalsState {
[ModalName.AccountSwitcher]: AppModalState<undefined>
[ModalName.ExchangeTransferModal]: AppModalState<{
serviceProvider: FORTransferInstitution
}>
[ModalName.Experiments]: AppModalState<undefined>
[ModalName.Explore]: AppModalState<ExploreModalState>
[ModalName.FiatCurrencySelector]: AppModalState<undefined>
......
......@@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { ExploreModalState } from 'src/app/modals/ExploreModalState'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { Screens } from 'src/screens/Screens'
import { getKeys } from 'utilities/src/primitives/objects'
......@@ -14,6 +15,11 @@ type AccountSwitcherModalParams = {
initialState?: undefined
}
type ExchangeTransferModalParams = {
name: typeof ModalName.ExchangeTransferModal
initialState?: ExchangeTransferModalState
}
type ExperimentsModalParams = { name: typeof ModalName.Experiments; initialState?: undefined }
type ExploreModalParams = {
......@@ -76,6 +82,7 @@ type ViewOnlyExplainerParams = {
export type OpenModalParams =
| AccountSwitcherModalParams
| ExchangeTransferModalParams
| ExperimentsModalParams
| ExploreModalParams
| FiatCurrencySelectorParams
......@@ -95,6 +102,10 @@ export type OpenModalParams =
export type CloseModalParams = { name: keyof ModalsState }
export const initialModalState: ModalsState = {
[ModalName.ExchangeTransferModal]: {
isOpen: false,
initialState: undefined,
},
[ModalName.FiatOnRamp]: {
isOpen: false,
initialState: undefined,
......
......@@ -2,44 +2,27 @@
exports[`renders collection preview card 1`] = `
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
disabled={false}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
......@@ -54,6 +54,7 @@ export enum MobileEventName {
FiatOnRampQuickActionButtonPressed = 'Fiat OnRamp QuickAction Button Pressed',
FiatOnRampAmountEntered = 'Fiat OnRamp Amount Entered',
FiatOnRampWidgetOpened = 'Fiat OnRamp Widget Opened',
NotificationsToggled = 'Notifications Toggled',
OnboardingCompleted = 'Onboarding Completed',
PerformanceReport = 'Performance Report',
PerformanceGraphql = 'Performance GraphQL',
......@@ -86,6 +87,8 @@ export enum UserPropertyName {
AppOpenAuthMethod = 'app_open_auth_method',
AppVersion = 'app_version',
DarkMode = 'is_dark_mode',
HasLoadedENS = 'has_loaded_ens',
HasLoadedUnitag = 'has_loaded_unitag',
IsCloudBackedUp = 'is_cloud_backed_up',
IsHideSmallBalancesEnabled = 'is_hide_small_balances_enabled',
IsHideSpamTokensEnabled = 'is_hide_spam_tokens_enabled',
......
......@@ -45,10 +45,27 @@ export const slice = createSlice({
state.walletIsFunded = true
},
setAllowAnalytics: (state, { payload: { enabled } }: PayloadAction<{ enabled: boolean }>) => {
sendWalletAnalyticsEvent(SharedEventName.ANALYTICS_SWITCH_TOGGLED, { enabled })
analytics.flushEvents()
// eslint-disable-next-line no-void
void analytics.setAllowAnalytics(enabled).finally(() => undefined)
const logToggleEvent = (): void => {
sendWalletAnalyticsEvent(SharedEventName.ANALYTICS_SWITCH_TOGGLED, { enabled })
analytics.flushEvents()
}
// If turning off, log toggle event before turning off analytics
if (!enabled) {
logToggleEvent()
}
analytics
.setAllowAnalytics(enabled)
.then(() => {
// If turned on, log toggle event after turning on analytics
if (enabled) {
logToggleEvent()
}
})
.catch(() => undefined)
// Set enabled in user state
state.allowAnalytics = enabled
},
},
......
......@@ -68,6 +68,9 @@ export type MobileEventProperties = {
[MobileEventName.FiatOnRampBannerPressed]: TraceProps
[MobileEventName.FiatOnRampAmountEntered]: TraceProps & { source: 'chip' | 'textInput' }
[MobileEventName.FiatOnRampWidgetOpened]: TraceProps & { externalTransactionId: string }
[MobileEventName.NotificationsToggled]: TraceProps & {
enabled: boolean
}
[MobileEventName.OnboardingCompleted]: OnboardingCompletedProps & TraceProps
[MobileEventName.PerformanceReport]: RenderPassReport
[MobileEventName.PerformanceGraphql]: {
......
import { useCallback } from 'react'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api'
import { useAppDispatch } from 'wallet/src/state'
import { ModalName } from 'wallet/src/telemetry/constants'
export function useOnSendEmptyActionPress(): () => void {
const { data } = useFiatOnRampIpAddressQuery()
const dispatch = useAppDispatch()
const fiatOnRampEligible = Boolean(data?.isBuyAllowed)
return useCallback((): void => {
dispatch(closeModal({ name: ModalName.Send }))
if (fiatOnRampEligible) {
dispatch(openModal({ name: ModalName.FiatOnRamp }))
} else {
dispatch(
openModal({
name: ModalName.WalletConnectScan,
initialState: ScannerModalState.WalletQr,
})
)
}
}, [dispatch, fiatOnRampEligible])
}
......@@ -8,7 +8,6 @@ import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect'
import Trace from 'src/components/Trace/Trace'
import { Screen } from 'src/components/layout/Screen'
import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks'
import { useOnSendEmptyActionPress } from 'src/features/transactions/hooks/useOnSendEmptyActionPress'
import { TransferHeader } from 'src/features/transactions/transfer/TransferHeader'
import { TransferStatus } from 'src/features/transactions/transfer/TransferStatus'
import { useWalletRestore } from 'src/features/wallet/hooks'
......@@ -107,7 +106,6 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
dispatch,
TokenSelectorFlow.Transfer
)
const onSendEmptyActionPress = useOnSendEmptyActionPress()
// optimization for not rendering InnerContent initially,
// when modal is opened with recipient or token selector presented
......@@ -211,7 +209,6 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
variation={TokenSelectorVariation.BalancesOnly}
onClose={onHideTokenSelector}
onSelectCurrency={onSelectCurrency}
onSendEmptyActionPress={onSendEmptyActionPress}
/>
)}
</>
......
......@@ -51,7 +51,6 @@ const FIXED_INFO_PILL_WIDTH = 128
// Used in dynamic font size width calculation to ignore `.` characters
const UNITAG_SUFFIX_CHARS_ONLY = UNITAG_SUFFIX.replaceAll('.', '')
const TEXT_INPUT_PLACEHOLDER = 'yourname'
// Accounts for height of image, gap between image and name, and spacing from top of titles
const UNITAG_NAME_ANIMATE_DISTANCE_Y = imageSizes.image100 + spacing.spacing48 + spacing.spacing24
......@@ -65,6 +64,8 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
const inputPlaceholder = t('yourname')
// In onboarding flow, delete pending accounts and create account actions happen right before navigation
// So pendingAccountAddress must be fetched in this component and can't be passed in params
const pendingAccountAddress = Object.values(usePendingAccounts())?.[0]?.address
......@@ -139,14 +140,14 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
}
if (text.length === 0) {
onSetFontSize(TEXT_INPUT_PLACEHOLDER + UNITAG_SUFFIX_CHARS_ONLY)
onSetFontSize(inputPlaceholder + UNITAG_SUFFIX_CHARS_ONLY)
} else {
onSetFontSize(text + UNITAG_SUFFIX_CHARS_ONLY)
}
setUnitagInputValue(text?.trim().toLowerCase())
},
[onSetFontSize, setUnitagInputValue]
[inputPlaceholder, onSetFontSize]
)
const onPressAddressTooltip = (): void => {
......@@ -256,7 +257,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
gap="$spacing16"
onLayout={(event): void => {
onLayout(event)
onSetFontSize(TEXT_INPUT_PLACEHOLDER + UNITAG_SUFFIX_CHARS_ONLY)
onSetFontSize(inputPlaceholder + UNITAG_SUFFIX_CHARS_ONLY)
}}>
{/* Fixed text that animates in when TextInput is animated out */}
<AnimatedFlex
......@@ -293,7 +294,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
fontWeight="$large"
numberOfLines={1}
p="$none"
placeholder={TEXT_INPUT_PLACEHOLDER}
placeholder={inputPlaceholder}
placeholderTextColor="$neutral3"
returnKeyType="done"
textAlign="left"
......@@ -396,6 +397,7 @@ const InfoModal = ({
}): JSX.Element => {
const colors = useSporeColors()
const { t } = useTranslation()
const usernamePlaceholder = t('yourname')
return (
<WarningModal
......@@ -429,7 +431,7 @@ const InfoModal = ({
shadowOpacity={0.4}
shadowRadius="$spacing4">
<Text color="$accent1" variant="buttonLabel4">
{TEXT_INPUT_PLACEHOLDER}
{usernamePlaceholder}
<Text color="$neutral2" variant="buttonLabel4">
{UNITAG_SUFFIX}
</Text>
......
......@@ -12,7 +12,7 @@ import { ChangeUnitagModal } from 'src/components/unitags/ChangeUnitagModal'
import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal'
import { DeleteUnitagModal } from 'src/components/unitags/DeleteUnitagModal'
import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture'
import { HeaderRadial } from 'src/features/externalProfile/ProfileHeader'
import { HeaderRadial, solidHeaderProps } from 'src/features/externalProfile/ProfileHeader'
import { Screens, UnitagScreens } from 'src/screens/Screens'
import {
Button,
......@@ -341,8 +341,7 @@ export function EditUnitagProfileScreen({
<HeaderRadial
borderRadius={spacing.spacing20}
color={avatarColors.primary}
maxOpacity={0.1}
minOpacity={0.1}
{...solidHeaderProps}
/>
) : null}
</Flex>
......
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getCountry } from 'react-native-localize'
import { useAppDispatch } from 'src/app/hooks'
import {
FiatOnRampConnectingView,
SERVICE_PROVIDER_ICON_BORDER_RADIUS,
SERVICE_PROVIDER_ICON_SIZE,
} from 'src/features/fiatOnRamp/FiatOnRampConnecting'
import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing'
import { uniswapUrls } from 'wallet/src/constants/urls'
import { useFiatOnRampAggregatorTransferWidgetQuery } from 'wallet/src/features/fiatOnRamp/api'
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
import { RemoteImage } from 'wallet/src/features/images/RemoteImage'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
import { openUri } from 'wallet/src/utils/linking'
import { isAndroid } from 'wallet/src/utils/platform'
// Design decision
const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS
const DEFAULT_TRANSFER_AMOUNT = 1
const DEFAULT_TRANSFER_CURRENCY = 'ETH'
export function ExchangeTransferConnecting({
serviceProvider,
onClose,
}: {
serviceProvider: FORTransferInstitution
onClose: () => void
}): JSX.Element {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const activeAccountAddress = useActiveAccountAddressWithThrow()
const [timeoutElapsed, setTimeoutElapsed] = useState(false)
const initialTypeInfo = useMemo(
() => ({ institutionLogoUrl: serviceProvider.icon }),
[serviceProvider.icon]
)
const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator(
activeAccountAddress,
initialTypeInfo
)
const onError = useCallback((): void => {
dispatch(
pushNotification({
type: AppNotificationType.Error,
errorMessage: t('Something went wrong.'),
})
)
onClose()
}, [dispatch, onClose, t])
useTimeout(() => {
setTimeoutElapsed(true)
}, CONNECTING_TIMEOUT)
const {
data: widgetData,
isLoading: widgetLoading,
error: widgetError,
} = useFiatOnRampAggregatorTransferWidgetQuery({
sourceAmount: DEFAULT_TRANSFER_AMOUNT,
sourceCurrencyCode: DEFAULT_TRANSFER_CURRENCY,
countryCode: getCountry(),
institutionId: serviceProvider.id,
walletAddress: activeAccountAddress,
externalSessionId: externalTransactionId,
redirectURL: `${
isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl
}/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`,
})
useEffect(() => {
if (widgetError) {
onError()
return
}
async function navigateToWidget(widgetUrl: string): Promise<void> {
onClose()
await openUri(widgetUrl).catch(onError)
dispatchAddTransaction()
}
if (timeoutElapsed && !widgetLoading && widgetData) {
navigateToWidget(widgetData.widgetUrl).catch(() => undefined)
}
}, [
dispatchAddTransaction,
onClose,
onError,
timeoutElapsed,
widgetData,
widgetLoading,
widgetError,
])
return (
<FiatOnRampConnectingView
serviceProviderLogo={
<RemoteImage
borderRadius={SERVICE_PROVIDER_ICON_BORDER_RADIUS}
height={SERVICE_PROVIDER_ICON_SIZE}
uri={serviceProvider.icon}
width={SERVICE_PROVIDER_ICON_SIZE}
/>
}
serviceProviderName={serviceProvider.name}
/>
)
}
......@@ -50,7 +50,7 @@ export function ExploreScreen(): JSX.Element {
const [isSearchMode, setIsSearchMode] = useState<boolean>(false)
const textInputRef = useRef<TextInput>(null)
const onChangeSearchFilter = (newSearchFilter: string): void => {
const onSearchChangeText = (newSearchFilter: string): void => {
setSearchQuery(newSearchFilter)
}
......@@ -83,9 +83,8 @@ export function ExploreScreen(): JSX.Element {
backgroundColor={isSearchMode ? contrastBackgroundColor : searchBarBackgroundColor}
placeholder={t('Search tokens and wallets')}
showShadow={!isSearchMode}
value={searchQuery}
onCancel={onSearchCancel}
onChangeText={onChangeSearchFilter}
onChangeText={onSearchChangeText}
onFocus={onSearchFocus}
/>
</Flex>
......
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { skipToken } from '@reduxjs/toolkit/query/react'
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import { useAppDispatch } from 'src/app/hooks'
import { FiatOnRampStackParamList } from 'src/app/navigation/types'
import { Loader } from 'src/components/loading'
import {
FiatOnRampConnectingView,
SERVICE_PROVIDER_ICON_SIZE,
......@@ -12,15 +12,25 @@ import {
import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext'
import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks'
import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/utils'
import { closeModal } from 'src/features/modals/modalSlice'
import { FiatOnRampScreens } from 'src/screens/Screens'
import { Flex, useIsDarkMode } from 'ui/src'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing'
import { uniswapUrls } from 'wallet/src/constants/urls'
import { useFiatOnRampAggregatorWidgetQuery } from 'wallet/src/features/fiatOnRamp/api'
import {
MELD_ICON_SIZE_MULTIPLIER,
getServiceProviderLogo,
} from 'wallet/src/features/fiatOnRamp/utils'
import { ImageUri } from 'wallet/src/features/images/ImageUri'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
import { ModalName } from 'wallet/src/telemetry/constants'
import { openUri } from 'wallet/src/utils/linking'
import { isAndroid } from 'wallet/src/utils/platform'
// Design decision
const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS
......@@ -34,12 +44,20 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
const [timeoutElapsed, setTimeoutElapsed] = useState(false)
const activeAccountAddress = useActiveAccountAddressWithThrow()
const { externalTransactionId, dispatchAddTransaction } =
useFiatOnRampTransactionCreator(activeAccountAddress)
const { selectedQuote, serviceProviders, countryCode, baseCurrencyInfo, quoteCurrency, amount } =
useFiatOnRampContext()
const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders)
const initialTypeInfo = useMemo(
() => ({ serviceProviderLogo: serviceProvider?.logos }),
[serviceProvider?.logos]
)
const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator(
activeAccountAddress,
initialTypeInfo
)
const onError = useCallback((): void => {
dispatch(
pushNotification({
......@@ -64,6 +82,9 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
sourceCurrencyCode: baseCurrencyInfo.code,
walletAddress: activeAccountAddress,
externalSessionId: externalTransactionId,
redirectUrl: `${
isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl
}/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`,
}
: skipToken
)
......@@ -77,11 +98,14 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
onError()
return
}
async function navigateToWidget(widgetUrl: string): Promise<void> {
dispatch(closeModal({ name: ModalName.FiatOnRampAggregator }))
await openUri(widgetUrl).catch(onError)
dispatchAddTransaction()
}
if (timeoutElapsed && !widgetLoading && widgetData) {
navigation.goBack()
openUri(widgetData.widgetUrl).catch(onError)
// TODO: Uncomment this when https://linear.app/uniswap/issue/MOB-2585/implement-polling-of-transaction-once-user-has-checked-out is implmented
// dispatchAddTransaction()
navigateToWidget(widgetData.widgetUrl).catch(() => undefined)
}
}, [
navigation,
......@@ -93,8 +117,12 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
dispatchAddTransaction,
baseCurrencyInfo,
serviceProvider,
dispatch,
])
const isDarkMode = useIsDarkMode()
const logoUrl = getServiceProviderLogo(serviceProvider?.logos, isDarkMode)
return baseCurrencyInfo && serviceProvider ? (
<FiatOnRampConnectingView
amount={addFiatSymbolToNumber({
......@@ -104,13 +132,22 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
})}
quoteCurrencyCode={quoteCurrency.currencyInfo?.currency.symbol}
serviceProviderLogo={
<Loader.Box
borderRadius="$rounded20"
<Flex
alignItems="center"
height={SERVICE_PROVIDER_ICON_SIZE}
width={SERVICE_PROVIDER_ICON_SIZE}
/>
justifyContent="center"
width={SERVICE_PROVIDER_ICON_SIZE}>
<ImageUri imageStyle={ServiceProviderLogoStyles.icon} uri={logoUrl} />
</Flex>
}
serviceProviderName={serviceProvider.name}
/>
) : null
}
const ServiceProviderLogoStyles = StyleSheet.create({
icon: {
height: SERVICE_PROVIDER_ICON_SIZE * MELD_ICON_SIZE_MULTIPLIER,
width: SERVICE_PROVIDER_ICON_SIZE * MELD_ICON_SIZE_MULTIPLIER,
},
})
......@@ -2,6 +2,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'
import React, { ComponentProps, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TextInput, TextInputProps } from 'react-native'
import FastImage from 'react-native-fast-image'
import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated'
import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks'
import { FiatOnRampStackParamList } from 'src/app/navigation/types'
......@@ -23,13 +24,17 @@ import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants'
import { MobileEventProperties } from 'src/features/telemetry/types'
import { FiatOnRampScreens } from 'src/screens/Screens'
import { AnimatedFlex, Flex, Text } from 'ui/src'
import { AnimatedFlex, Flex, Text, useIsDarkMode } from 'ui/src'
import { usePrevious } from 'utilities/src/react/hooks'
import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy'
import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext'
import { HandleBar } from 'wallet/src/components/modals/HandleBar'
import { useFiatOnRampAggregatorServiceProvidersQuery } from 'wallet/src/features/fiatOnRamp/api'
import { FORQuote } from 'wallet/src/features/fiatOnRamp/types'
import {
useFiatOnRampAggregatorServiceProvidersQuery,
useFiatOnRampAggregatorTransactionsQuery,
} from 'wallet/src/features/fiatOnRamp/api'
import { FORQuote, FORServiceProvider, FORTransaction } from 'wallet/src/features/fiatOnRamp/types'
import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
......@@ -37,29 +42,45 @@ type Props = NativeStackScreenProps<FiatOnRampStackParamList, FiatOnRampScreens.
function selectInitialQuote(
quotes: FORQuote[] | undefined,
lastTransaction: undefined
lastTransaction: FORTransaction | undefined
): { quote: FORQuote | undefined; type: InitialQuoteSelection | undefined } {
if (lastTransaction) {
// setting "Recently used"
// TODO:https://linear.app/uniswap/issue/MOB-2533/implement-recently-used-logic
} else {
// setting "Best overall"
const initialQuote = quotes && quotes.length && quotes[0]
if (initialQuote) {
const lastUsedServiceProvider = lastTransaction?.serviceProvider
if (lastUsedServiceProvider) {
const quote = quotes?.filter((q) => q.serviceProvider === lastUsedServiceProvider)[0]
if (quote) {
return {
quote: quotes.reduce<FORQuote>((prev, curr) => {
return curr.destinationAmount > prev.destinationAmount ? curr : prev
}, initialQuote),
type: InitialQuoteSelection.Best,
quote,
type: InitialQuoteSelection.MostRecent,
}
}
}
const bestQuote = quotes && quotes.length && quotes[0]
if (bestQuote) {
return {
quote: quotes.reduce<FORQuote>((prev, curr) => {
return curr.destinationAmount > prev.destinationAmount ? curr : prev
}, bestQuote),
type: InitialQuoteSelection.Best,
}
}
return { quote: undefined, type: undefined }
}
function preloadServiceProviderLogos(
serviceProviders: FORServiceProvider[],
isDarkMode: boolean
): void {
FastImage.preload(
serviceProviders
.map((sp) => ({ uri: getServiceProviderLogo(sp.logos, isDarkMode) }))
.filter((sp) => !!sp.uri)
)
}
export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const isDarkMode = useIsDarkMode()
const [selection, setSelection] = useState<TextInputProps['selection']>()
const [value, setValue] = useState('')
const [showTokenSelector, setShowTokenSelector] = useState(false)
......@@ -109,6 +130,21 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
error: serviceProvidersError,
} = useFiatOnRampAggregatorServiceProvidersQuery()
// preload service provider logos for given quotes for the next screen
useEffect(() => {
if (serviceProvidersResponse?.serviceProviders && quotes) {
const quotesServiceProviderNames = quotes.map((q) => q.serviceProvider)
const serviceProviders = serviceProvidersResponse.serviceProviders.filter(
(sp) => quotesServiceProviderNames.indexOf(sp.serviceProvider) !== -1
)
preloadServiceProviderLogos(serviceProviders, isDarkMode)
}
}, [serviceProvidersResponse, quotes, isDarkMode])
const { currentData: transactionsResponse } = useFiatOnRampAggregatorTransactionsQuery({
limit: 1,
})
const { errorText, errorColor } = useParseFiatOnRampError(
quotesError || serviceProvidersError,
meldSupportedFiatCurrency.code
......@@ -117,7 +153,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
const prevQuotes = usePrevious(quotes)
useEffect(() => {
if (quotes && (!selectedQuote || prevQuotes !== quotes)) {
const { quote, type } = selectInitialQuote(quotes, undefined)
const { quote, type } = selectInitialQuote(quotes, transactionsResponse?.transactions[0])
if (!quote) {
return
}
......@@ -128,7 +164,15 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
])
setSelectedQuote(quote)
}
}, [prevQuotes, quotes, selectedQuote, setQuotesSections, setSelectedQuote, t])
}, [
prevQuotes,
quotes,
selectedQuote,
setQuotesSections,
setSelectedQuote,
t,
transactionsResponse?.transactions,
])
useEffect(() => {
if (!quotes && (quotesError || serviceProvidersError || !amount)) {
......
......@@ -13,7 +13,7 @@ import { InitialQuoteSelection } from 'src/features/fiatOnRamp/types'
import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/utils'
import { MobileEventName } from 'src/features/telemetry/constants'
import { FiatOnRampScreens } from 'src/screens/Screens'
import { AnimatedFlex, Button, Flex, Icons, Inset, Separator, Text } from 'ui/src'
import { AnimatedFlex, Button, Flex, GeneratedIcon, Icons, Inset, Separator, Text } from 'ui/src'
import { Trace } from 'utilities/src/telemetry/trace/Trace'
import { HandleBar } from 'wallet/src/components/modals/HandleBar'
import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks'
......@@ -24,6 +24,17 @@ type Props = NativeStackScreenProps<FiatOnRampStackParamList, FiatOnRampScreens.
const key = (item: FORQuote): string => item.serviceProvider
function SectionHeader({ Icon, title }: { Icon: GeneratedIcon; title: string }): JSX.Element {
return (
<Flex row alignItems="center" pl="$spacing8">
<Icon color="$neutral3" size="$icon.16" />
<Text color="$neutral2" pl="$spacing4" variant="body3">
{title}
</Text>
</Flex>
)
}
export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Element {
const { t } = useTranslation()
const {
......@@ -59,28 +70,23 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele
section: { type },
}: {
section: SectionListData<FORQuote, { type?: InitialQuoteSelection }>
}): JSX.Element => {
return (
<Flex px="$spacing12">
{type ? (
<Flex row alignItems="center" pl="$spacing8">
<Icons.Verified color="$accent1" size="$icon.16" />
<Text color="$neutral2" pl="$spacing4" variant="body3">
{type === InitialQuoteSelection.Best ? t('Best overall') : t('Recently used')}
</Text>
</Flex>
) : (
<Flex centered row gap="$spacing12" my="$spacing12">
<Separator />
<Text color="$neutral3" variant="body3">
{t('Other options')}
</Text>
<Separator />
</Flex>
)}
</Flex>
)
}
}): JSX.Element => (
<Flex px="$spacing12">
{type === InitialQuoteSelection.Best ? (
<SectionHeader Icon={Icons.Verified} title={t('Best overall')} />
) : type === InitialQuoteSelection.MostRecent ? (
<SectionHeader Icon={Icons.TimePast} title={t('Recently used')} />
) : (
<Flex centered row gap="$spacing12" my="$spacing12">
<Separator />
<Text color="$neutral3" variant="body3">
{t('Other options')}
</Text>
<Separator />
</Flex>
)}
</Flex>
)
const onContinue = (): void => {
const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders)
......
......@@ -6,14 +6,10 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Freeze } from 'react-freeze'
import { useTranslation } from 'react-i18next'
import { FlatList, StyleProp, View, ViewProps, ViewStyle } from 'react-native'
import { TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler'
import Animated, {
FadeIn,
FadeOut,
cancelAnimation,
interpolateColor,
runOnJS,
useAnimatedGestureHandler,
useAnimatedRef,
useAnimatedScrollHandler,
useAnimatedStyle,
......@@ -29,7 +25,6 @@ import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import Trace from 'src/components/Trace/Trace'
import TraceTabView from 'src/components/Trace/TraceTabView'
import { AccountHeader } from 'src/components/accounts/AccountHeader'
import { pulseAnimation } from 'src/components/buttons/utils'
import { ACTIVITY_TAB_DATA_DEPENDENCIES, ActivityTab } from 'src/components/home/ActivityTab'
import { FEED_TAB_DATA_DEPENDENCIES, FeedTab } from 'src/components/home/FeedTab'
import { NFTS_TAB_DATA_DEPENDENCIES, NftsTab } from 'src/components/home/NftsTab'
......@@ -729,44 +724,25 @@ function ActionButton({
iconScale?: number
}): JSX.Element {
const colors = useSporeColors()
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(
() => ({
transform: [{ scale: scale.value }],
}),
[scale]
)
const media = useMedia()
const iconSize = media.short ? iconSizes.icon24 : iconSizes.icon28
const onGestureEvent = useAnimatedGestureHandler<TapGestureHandlerGestureEvent>({
onStart: () => {
cancelAnimation(scale)
scale.value = pulseAnimation(activeScale)
},
onEnd: () => {
runOnJS(onPress)()
},
})
return (
<Trace logPress element={name} pressEvent={eventName}>
<TouchableArea hapticFeedback flex={flex} onPress={onPress}>
<TapGestureHandler onGestureEvent={onGestureEvent}>
<AnimatedFlex
centered
fill
backgroundColor="$DEP_backgroundActionButton"
borderRadius="$rounded20"
p="$spacing16"
style={animatedStyle}>
<Icon
color={colors.accent1.get()}
height={iconSize * iconScale}
strokeWidth={2}
width={iconSize * iconScale}
/>
</AnimatedFlex>
</TapGestureHandler>
<TouchableArea hapticFeedback flex={flex} scaleTo={activeScale} onPress={onPress}>
<AnimatedFlex
centered
fill
backgroundColor="$DEP_backgroundActionButton"
borderRadius="$rounded20"
p="$spacing16">
<Icon
color={colors.accent1.get()}
height={iconSize * iconScale}
strokeWidth={2}
width={iconSize * iconScale}
/>
</AnimatedFlex>
</TouchableArea>
</Trace>
)
......
......@@ -4,17 +4,16 @@ import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppDispatch } from 'src/app/hooks'
import { OnboardingStackParamList } from 'src/app/navigation/types'
import { Loader } from 'src/components/loading'
import { clearCloudBackups } from 'src/features/CloudBackup/cloudBackupSlice'
import { useCloudBackups } from 'src/features/CloudBackup/hooks'
import {
startFetchingCloudStorageBackups,
stopFetchingCloudStorageBackups,
} from 'src/features/CloudBackup/RNCloudStorageBackupsManager'
import { clearCloudBackups } from 'src/features/CloudBackup/cloudBackupSlice'
import { useCloudBackups } from 'src/features/CloudBackup/hooks'
import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { OnboardingScreens } from 'src/screens/Screens'
import { useAddBackButton } from 'src/utils/useAddBackButton'
import { Flex, Icons } from 'ui/src'
import { Flex, Icons, Loader } from 'ui/src'
import { imageSizes } from 'ui/src/theme'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
......
......@@ -4,14 +4,13 @@ import { useTranslation } from 'react-i18next'
import { ScrollView } from 'react-native-gesture-handler'
import { useAppDispatch } from 'src/app/hooks'
import { OnboardingStackParamList } from 'src/app/navigation/types'
import { Loader } from 'src/components/loading'
import WalletPreviewCard from 'src/features/import/WalletPreviewCard'
import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { OnboardingScreens } from 'src/screens/Screens'
import { Button, Flex } from 'ui/src'
import { Button, Flex, Loader } from 'ui/src'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPreviewCard'
import { useSelectWalletScreenQuery } from 'wallet/src/data/__generated__/types-and-hooks'
import { ImportType } from 'wallet/src/features/onboarding/types'
import {
......
......@@ -227,48 +227,30 @@ exports[`RestoreCloudBackupPasswordScreen renders correctly 1`] = `
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"paddingBottom": 4,
"paddingLeft": 4,
"paddingRight": 4,
"paddingTop": 4,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
......@@ -19,7 +19,7 @@ const ACCOUNT_IMAGE_SIZE = 52
const ICON_SIZE = 32
const ICON_BORDER_RADIUS = 100
function AccountCardItem(): JSX.Element {
function AccountCardItem({ onClose }: { onClose: () => void }): JSX.Element {
const dispatch = useAppDispatch()
const activeAccountAddress = useActiveAccountAddressWithThrow()
......@@ -35,49 +35,49 @@ function AccountCardItem(): JSX.Element {
}
const onPressShowWalletQr = (): void => {
dispatch(closeModal({ name: ModalName.ReceiveCryptoModal }))
onClose()
dispatch(
openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })
)
}
return (
<Flex row alignItems="flex-start" gap="$spacing12" px="$spacing8">
<Flex
fill
row
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth="$spacing1"
gap="$spacing12"
p="$spacing12">
<Flex fill>
<AddressDisplay
address={activeAccountAddress}
captionVariant="body3"
gapBetweenLines="$spacing2"
size={ACCOUNT_IMAGE_SIZE}
/>
</Flex>
<Flex centered row gap="$spacing12" px="$spacing8">
<TouchableArea
hapticFeedback
hapticStyle={ImpactFeedbackStyle.Light}
onPress={onPressCopyAddress}>
<Flex
centered
row
backgroundColor="$surface3"
borderRadius={ICON_BORDER_RADIUS}
height={ICON_SIZE}
width={ICON_SIZE}>
<Icons.CopySheets color="$neutral2" size={iconSizes.icon16} />
</Flex>
</TouchableArea>
<TouchableArea
hapticFeedback
hapticStyle={ImpactFeedbackStyle.Light}
onPress={onPressShowWalletQr}>
<TouchableArea
hapticFeedback
hapticStyle={ImpactFeedbackStyle.Light}
onPress={onPressShowWalletQr}>
<Flex row alignItems="flex-start" gap="$spacing12" px="$spacing8">
<Flex
fill
row
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth="$spacing1"
gap="$spacing12"
p="$spacing12">
<Flex fill>
<AddressDisplay
address={activeAccountAddress}
captionVariant="body3"
gapBetweenLines="$spacing2"
size={ACCOUNT_IMAGE_SIZE}
/>
</Flex>
<Flex centered row gap="$spacing12" px="$spacing8">
<TouchableArea
hapticFeedback
hapticStyle={ImpactFeedbackStyle.Light}
onPress={onPressCopyAddress}>
<Flex
centered
row
backgroundColor="$surface3"
borderRadius={ICON_BORDER_RADIUS}
height={ICON_SIZE}
width={ICON_SIZE}>
<Icons.CopySheets color="$neutral2" size={iconSizes.icon16} />
</Flex>
</TouchableArea>
<Flex
centered
row
......@@ -87,10 +87,10 @@ function AccountCardItem(): JSX.Element {
width={ICON_SIZE}>
<Icons.QrCode color="$neutral2" size={iconSizes.icon16} />
</Flex>
</TouchableArea>
</Flex>
</Flex>
</Flex>
</Flex>
</TouchableArea>
)
}
......@@ -119,18 +119,18 @@ export function ReceiveCryptoModal(): JSX.Element {
{t('Receive crypto')}
</Text>
<Text color="$neutral2" mt="$spacing2" textAlign="center" variant="body3">
{t('Deposit funds from another wallet or an account')}
{t('Fund your wallet by transferring crypto from another wallet or account')}
</Text>
</Flex>
<AccountCardItem />
<AccountCardItem onClose={onClose} />
<Flex centered row shrink gap="$spacing12" py="$spacing8">
<Separator />
<Text color="$neutral2" textAlign="center" variant="body3">
{t('From an account')}
{t('Link an account')}
</Text>
<Separator />
</Flex>
<TransferInstitutionSelector />
<TransferInstitutionSelector onClose={onClose} />
</Flex>
</BottomSheetModal>
)
......
......@@ -25,6 +25,8 @@ import {
useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { promptPushPermission } from 'src/features/notifications/Onesignal'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants'
import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen'
import { Screens, UnitagScreens } from 'src/screens/Screens'
import { Button, Flex, Text, useSporeColors } from 'ui/src'
......@@ -96,6 +98,7 @@ export function SettingsWallet({
)
const onChangeNotificationSettings = (enabled: boolean): void => {
sendMobileAnalyticsEvent(MobileEventName.NotificationsToggled, { enabled })
if (notificationOSPermission === NotificationPermission.Enabled) {
dispatch(
editAccountActions.trigger({
......
......@@ -18,7 +18,7 @@ import { TextInput } from 'wallet/src/components/input/TextInput'
import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts'
import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants'
import { useFeatureFlag } from 'wallet/src/features/experiments/hooks'
import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks'
import { useCanAddressClaimUnitag } from 'wallet/src/features/unitags/hooks'
import {
EditAccountAction,
editAccountActions,
......@@ -43,7 +43,7 @@ export function SettingsWalletEdit({
const [nickname, setNickname] = useState(displayName?.name)
const [showEditButton, setShowEditButton] = useState(true)
const unitagsFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags)
const { canClaimUnitag } = useCanActiveAddressClaimUnitag()
const { canClaimUnitag } = useCanAddressClaimUnitag(address)
const showUnitagBanner =
unitagsFeatureFlagEnabled &&
activeAccount?.type === AccountType.SignerMnemonic &&
......
......@@ -105,11 +105,10 @@ function HeaderTitleElement({
export function TokenDetailsScreen({
route,
navigation,
}: AppStackScreenProp<Screens.TokenDetails>): JSX.Element {
const { currencyId: _currencyId } = route.params
// Potentially delays loading of perf-heavy content to speed up navigation
const showSkeleton = useSkeletonLoading(navigation)
const showSkeleton = useSkeletonLoading()
const language = useCurrentLanguage()
// Token details screen query
......
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useEffect, useState } from 'react'
import { useExperiment } from 'statsig-react-native'
import { logger } from 'utilities/src/logger/logger'
import { useTimeout } from 'utilities/src/time/timing'
import { EXPERIMENT_NAMES } from 'wallet/src/features/experiments/constants'
const TIMEOUT_MS = 5000
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type NavigationProp = NativeStackNavigationProp<any, any, any | undefined>
export function useSkeletonLoading(navigation: NavigationProp): boolean {
const experiment = useExperiment(EXPERIMENT_NAMES.SkeletonLoading)
const focusBasedEnabled = !!experiment.config.getValue('enable_focus_based')
const transitionBasedEnabled = !!experiment.config.getValue('enable_transition_based')
const timeoutBasedEnabled = !!experiment.config.getValue('enable_timeout_based')
const [enabled, setEnabled] = useState(
focusBasedEnabled || transitionBasedEnabled || timeoutBasedEnabled
)
useTimeout(() => {
if (enabled) {
setEnabled(false)
logger.warn(
'useSkeletonLoading',
'useSkeletonLoading',
'Timeout reached to disable enabled state'
)
}
}, TIMEOUT_MS)
/**
* Utility hook used to delay rendering initially so that the screen render a skeleton of placeholders
* to allow navigation to progress before rendering heavier components that may appear as lag
*/
export function useSkeletonLoading(): boolean {
const [enabled, setEnabled] = useState(true)
useEffect(() => {
if (focusBasedEnabled) {
return navigation.addListener('focus', () => setEnabled(false))
} else if (transitionBasedEnabled) {
return navigation.addListener('transitionEnd', () => setEnabled(false))
} else if (timeoutBasedEnabled) {
setTimeout(() => setEnabled(false), 0)
}
}, [navigation, focusBasedEnabled, transitionBasedEnabled, timeoutBasedEnabled])
setTimeout(() => setEnabled(false), 0)
})
return enabled
}
......@@ -43,6 +43,7 @@ ignores: [
## Internal packages / workspaces
"wallet",
"utilities",
"ui",
## Top level local file paths
"abis",
"analytics",
......
......@@ -16,4 +16,5 @@ REACT_APP_TEMP_API_URL="https://temp.gateway.uniswap.org/v1"
REACT_APP_UNISWAP_API_URL="https://interface.gateway.uniswap.org/v2"
REACT_APP_UNISWAP_BASE_API_URL="https://interface.gateway.uniswap.org/"
REACT_APP_UNISWAP_GATEWAY_DNS="https://interface.gateway.uniswap.org/v2"
REACT_APP_UNITAGS_API_URL="https://gateway.uniswap.org/v2/unitags"
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"
......@@ -60,6 +60,7 @@ module.exports = {
transformIgnorePatterns: ['d3-array'],
moduleNameMapper: {
'd3-array': 'd3-array/dist/d3-array.min.js',
'^react-native$': 'react-native-web',
},
})
},
......
......@@ -38,14 +38,14 @@ describe('Landing Page', () => {
cy.viewport(2000, 1600)
cy.visit('/swap')
cy.get(getTestSelector('pool-nav-link')).first().click()
cy.url().should('include', '/pools')
cy.url().should('include', '/pool')
})
it('allows navigation to pool on mobile', () => {
cy.viewport('iphone-6')
cy.visit('/swap')
cy.get(getTestSelector('pool-nav-link')).last().click()
cy.url().should('include', '/pools')
cy.url().should('include', '/pool')
})
it('does not render landing page when / path is blocked', () => {
......
......@@ -3,7 +3,7 @@ describe('Link', () => {
it('should update route', () => {
cy.viewport(2000, 1600)
cy.visit('/swap')
cy.contains('Pools').click()
cy.contains('Pool').click()
cy.get('[data-cy="join-pool-button"]').should('exist')
})
})
......@@ -3,13 +3,107 @@ import { getTestSelector } from '../../utils'
describe('Uni tags support', () => {
beforeEach(() => {
const unitagSpy = cy.spy().as('unitagSpy')
cy.intercept(/gateway.uniswap.org\/v2\/address/, (req) => {
unitagSpy(req)
})
cy.visit('/swap', {
featureFlags: [{ name: FeatureFlag.uniTags, value: true }],
})
})
it('displays claim banner in account drawer', () => {
it('displays banner in account drawer', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.contains('Introducing uni.eth usernames')
})
it('displays large banner on page', () => {
cy.get(getTestSelector('large-unitag-banner')).should('be.visible')
})
it('does not display banner on landing page', () => {
cy.visit('/?intro=true', {
featureFlags: [{ name: FeatureFlag.uniTags, value: true }],
})
cy.get(getTestSelector('large-unitag-banner')).should('not.be.visible')
})
it('opens modal and hides itself when accept button is clicked', () => {
cy.get(getTestSelector('large-unitag-banner')).within(() => {
cy.get(getTestSelector('unitag-banner-accept-button')).click()
})
cy.contains('Download the Uniswap app').should('exist')
cy.get(getTestSelector('get-the-app-close-button')).click()
cy.get(getTestSelector('large-unitag-banner')).should('not.be.visible')
cy.get(getTestSelector('web3-status-connected')).click()
cy.contains('Claim your Uniswap username')
cy.get('Introducing uni.eth usernames').should('not.exist')
})
it('hides itself when reject button is clicked', () => {
cy.get(getTestSelector('large-unitag-banner')).within(() => {
cy.get(getTestSelector('unitag-banner-reject-button')).click()
})
cy.get(getTestSelector('large-unitag-banner')).should('not.be.visible')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get('Introducing uni.eth usernames').should('not.exist')
})
it('shows address if no Unitag or ENS exists', () => {
cy.hardhat().then(() => {
const unusedAccount = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [unusedAccount]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains('0xF030...69Fb').should('be.visible')
})
})
})
it('shows Unitag, followed by address, if Unitag exists but not ENS', () => {
cy.intercept(/address/, { fixture: 'mini-portfolio/unitag.json' })
cy.hardhat().then(() => {
const accountWithUnitag = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [accountWithUnitag]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains('hayden').should('be.visible')
cy.contains('0xF030...69Fb').should('be.visible')
})
})
})
it('shows ENS, followed by address, if ENS exists but not Unitag', () => {
cy.hardhat().then(() => {
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
const haydenENS = 'hayden.eth'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains(haydenENS).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
})
})
it('shows Unitag and more option if user has both Unitag and ENS', () => {
cy.intercept(/address/, { fixture: 'mini-portfolio/unitag.json' })
cy.hardhat().then(() => {
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
const haydenUnitag = 'hayden'
const haydenENS = 'hayden.eth'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains(haydenUnitag).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
cy.get(getTestSelector('secondary-identifiers'))
.trigger('mouseover')
.click()
.within(() => {
cy.contains(haydenENS).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
})
})
})
......@@ -30,11 +30,11 @@ describe('Navigation', () => {
cy.url().should('include', '/nfts')
})
it('displays Pools tab', () => {
it('displays Pool tab', () => {
cy.get('nav').within(() => {
cy.contains('Pools').should('be.visible').click()
cy.contains('Pool').should('be.visible').click()
})
cy.url().should('include', '/pools')
cy.url().should('include', '/pool')
})
describe('More Menu', () => {
......@@ -50,11 +50,11 @@ describe('Navigation', () => {
featureFlags: [{ name: FeatureFlag.landingPageV2, value: true }],
})
cy.get('nav').within(() => {
cy.contains('Pools').should('not.be.visible')
cy.contains('Pool').should('not.be.visible')
cy.get(getTestSelector('nav-more-button')).should('be.visible').click()
cy.get(getTestSelector('nav-more-menu')).within(() => {
cy.contains('Pools').should('be.visible').click()
cy.url().should('include', '/pools')
cy.contains('Pool').should('be.visible').click()
cy.url().should('include', '/pool')
})
})
})
......
......@@ -199,6 +199,7 @@ describe('Permit2', () => {
cy.contains('Approve and swap').click()
// Verify token approval
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(DAI)
......@@ -231,7 +232,6 @@ describe('Permit2', () => {
})
it('prompts signature when existing permit approval is expired', () => {
setupInputs(DAI, USDC_MAINNET)
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
cy.hardhat()
.then(({ approval, wallet }) =>
......@@ -241,7 +241,8 @@ describe('Permit2', () => {
])
)
.then(() => {
initiateSwap('Approve and swap')
setupInputs(DAI, USDC_MAINNET)
initiateSwap('Sign and swap')
})
// Verify permit2 approval
......@@ -251,7 +252,6 @@ describe('Permit2', () => {
})
it('prompts signature when existing permit approval amount is too low', () => {
setupInputs(DAI, USDC_MAINNET)
const smallAllowance = { amount: 1 }
cy.hardhat()
.then(({ approval, wallet }) =>
......@@ -261,7 +261,8 @@ describe('Permit2', () => {
])
)
.then(() => {
initiateSwap('Approve and swap')
setupInputs(DAI, USDC_MAINNET)
initiateSwap('Sign and swap')
})
// Verify permit2 approval
......
describe('Pool', () => {
beforeEach(() => {
cy.visit('/pools').then(() => {
cy.visit('/pool').then(() => {
cy.wait('@eth_blockNumber')
})
})
......
......@@ -53,6 +53,7 @@ describe('Swap', () => {
})
it('swaps ETH for USDC', () => {
cy.interceptGraphqlOperation('Activity', 'mini-portfolio/empty_activity.json')
cy.visit('/swap')
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
......
......@@ -163,7 +163,7 @@ describe('Wallet Dropdown', () => {
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-buy-crypto')).click()
cy.contains('Buy crypto').should('not.be.visible')
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
})
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
......
{
"username": "hayden"
}
\ No newline at end of file
......@@ -21,6 +21,12 @@ declare global {
* @returns {Chainable<Subject>}
*/
waitForAmplitudeEvent(eventName: string, requiredProperties?: string[]): Chainable<Subject>
/**
* Intercepts a specific graphql operation and responds with the given fixture.
* @param {string} operationName - The name of the graphql operation to intercept.
* @param {string} fixturePath - The path to the fixture to respond with.
*/
interceptGraphqlOperation(operationName: string, fixturePath: string): Chainable<Subject>
}
interface VisitOptions {
serviceWorker?: true
......@@ -96,3 +102,13 @@ Cypress.Commands.add('waitForAmplitudeEvent', (eventName, requiredProperties) =>
}
return findAndDiscardEventsUpToTarget()
})
Cypress.Commands.add('interceptGraphqlOperation', (operationName, fixturePath) => {
return cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v1\/graphql/, (req) => {
if (req.body.operationName === operationName) {
req.reply({ fixture: fixturePath })
} else {
req.continue()
}
})
})
const defaultUrls = ['http://127.0.0.1:3000/', 'http://127.0.0.1:3000/swap', 'http://127.0.0.1:3000/pools']
const defaultUrls = ['http://127.0.0.1:3000/', 'http://127.0.0.1:3000/swap', 'http://127.0.0.1:3000/pool']
test.each(defaultUrls)('should inject metadata for valid collections', async (defaultUrl) => {
const body = await fetch(new Request(defaultUrl)).then((res) => res.text())
......
......@@ -14,7 +14,7 @@ const forks = {
[ChainId.MAINNET]: {
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
// Temporarily hardcoding this to fix e2e tests as we investigate source of swap tests failing on older blocknumbers
blockNumber: 19164140,
blockNumber: 19270708,
...forkingConfig,
},
[ChainId.POLYGON]: {
......
......@@ -198,12 +198,12 @@
"@uniswap/merkle-distributor": "1.0.1",
"@uniswap/permit2-sdk": "1.2.0",
"@uniswap/redux-multicall": "1.1.8",
"@uniswap/router-sdk": "1.7.1",
"@uniswap/sdk-core": "4.0.7",
"@uniswap/router-sdk": "1.8.0",
"@uniswap/sdk-core": "4.1.2",
"@uniswap/smart-order-router": "3.17.3",
"@uniswap/token-lists": "1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "1.4.1",
"@uniswap/universal-router-sdk": "1.5.8",
"@uniswap/universal-router-sdk": "1.7.1",
"@uniswap/v2-core": "1.0.1",
"@uniswap/v2-periphery": "1.1.0-beta.0",
"@uniswap/v2-sdk": "4.1.0",
......
......@@ -18,6 +18,18 @@
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://app.uniswap.org/limit</loc>
<lastmod>2024-02-21T20:00:00.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://app.uniswap.org/limits</loc>
<lastmod>2024-02-21T20:00:00.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://app.uniswap.org/swap</loc>
<lastmod>2023-10-11T19:57:27.976Z</lastmod>
......
......@@ -10,7 +10,7 @@ const thegraphConfig = require('../graphql.thegraph.config')
const exec = promisify(child_process.exec)
function fetchSchema(url, outputFile) {
exec(`npx --silent get-graphql-schema --h Origin=https://app.uniswap.org ${url}`)
exec(`npx --silent get-graphql-schema -h Origin=https://app.uniswap.org ${url}`)
.then(({ stderr, stdout }) => {
if (stderr) {
throw new Error(stderr)
......
......@@ -152,7 +152,7 @@ export const AboutFooter = () => {
<TextLink to="/swap">Swap</TextLink>
<TextLink to="/tokens">Tokens</TextLink>
{!shouldDisableNFTRoutes && <TextLink to="/nfts">NFTs</TextLink>}
<TextLink to="/pools">Pools</TextLink>
<TextLink to="/pool">Pool</TextLink>
</LinkGroup>
<LinkGroup>
<LinkGroupTitle>Protocol</LinkGroupTitle>
......
......@@ -50,7 +50,7 @@ export const MORE_CARDS = [
elementName: InterfaceElementName.ABOUT_PAGE_BUY_CRYPTO_CARD,
},
{
to: '/pools',
to: '/pool',
title: 'Earn',
description: 'Provide liquidity to pools on Uniswap and earn fees on swaps.',
lightIcon: <StyledCardLogo src={lightArrowImgSrc} alt="Analytics" />,
......
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
import Column from 'components/Column'
import Row from 'components/Row'
import Tooltip from 'components/Tooltip'
import { SupportArticleURL } from 'constants/supportArticles'
import { ReactNode, useReducer } from 'react'
import { Info } from 'react-feather'
import { Text } from 'rebass'
import styled from 'styled-components'
import { ExternalLink } from 'theme/components'
import { ThemedText } from 'theme/components/text'
const Container = styled(Column)`
position: relative;
height: 100%;
width: 100%;
`
const Tile = styled(ThemeButton)`
height: 100%;
width: 100%;
display: flex;
justify-content: flex-start;
padding: 12px;
border-color: transparent;
border-radius: 16px;
border-style: solid;
border-width: 1px;
`
const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
height: 28px;
width: 28px;
fill: ${({ theme }) => theme.accent1};
`
const ActionName = styled(Text)`
font-size: 16px;
font-style: normal;
font-weight: 535;
line-height: 24px;
`
const ErrorContainer = styled(Row)`
width: 100%;
position: absolute;
bottom: -24px;
display: flex;
justify-content: center;
align-items: center;
`
const ErrorText = styled(ThemedText.LabelMicro)`
color: ${({ theme }) => theme.neutral2};
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`
const ErrorLink = styled(ExternalLink)`
align-items: center;
display: flex;
height: 14px;
justify-content: center;
margin-left: 6px;
width: 14px;
`
const StyledInfoIcon = styled(Info)`
height: 12px;
width: 12px;
flex: 1 1 auto;
stroke: ${({ theme }) => theme.neutral2};
`
export function ActionTile({
dataTestId,
Icon,
name,
onClick,
loading,
disabled,
error,
errorMessage,
errorTooltip,
}: {
dataTestId: string
Icon: ReactNode
name: string
onClick: () => void
loading?: boolean
disabled?: boolean
error?: boolean
errorMessage?: string
errorTooltip?: string
}) {
const [showTooltip, toggleTooltip] = useReducer((isOpen) => !isOpen, false)
return (
<Container>
<Tile
data-testid={dataTestId}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.highSoft}
onClick={onClick}
disabled={disabled}
>
<Column gap="12px">
{loading ? <StyledLoadingButtonSpinner /> : Icon}
<ActionName>{name}</ActionName>
</Column>
</Tile>
{error && (
<ErrorContainer>
<ErrorText>{errorMessage}</ErrorText>
<Tooltip show={showTooltip} text={errorTooltip}>
<ErrorLink
onMouseEnter={toggleTooltip}
onMouseLeave={toggleTooltip}
style={{ color: 'inherit' }}
href={SupportArticleURL.MOONPAY_REGIONAL_AVAILABILITY}
>
<StyledInfoIcon />
</ErrorLink>
</Tooltip>
</ErrorContainer>
)}
</Container>
)
}
......@@ -5,6 +5,7 @@ import WalletModal from 'components/WalletModal'
import { useCallback, useEffect, useMemo } from 'react'
import styled from 'styled-components'
import { sendAnalyticsEvent } from 'analytics'
import { atom, useAtom } from 'jotai'
import AuthenticatedHeader from './AuthenticatedHeader'
import LanguageMenu from './LanguageMenu'
......@@ -17,11 +18,11 @@ const DefaultMenuWrap = styled(Column)`
`
export enum MenuState {
DEFAULT,
SETTINGS,
LANGUAGE_SETTINGS,
LOCAL_CURRENCY_SETTINGS,
LIMITS,
DEFAULT = 'default',
SETTINGS = 'settings',
LANGUAGE_SETTINGS = 'language_settings',
LOCAL_CURRENCY_SETTINGS = 'local_currency_settings',
LIMITS = 'limits',
}
export const miniPortfolioMenuStateAtom = atom(MenuState.DEFAULT)
......@@ -35,7 +36,6 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) {
const closeSettings = useCallback(() => setMenu(MenuState.DEFAULT), [setMenu])
const openLanguageSettings = useCallback(() => setMenu(MenuState.LANGUAGE_SETTINGS), [setMenu])
const openLocalCurrencySettings = useCallback(() => setMenu(MenuState.LOCAL_CURRENCY_SETTINGS), [setMenu])
const openLimitsMenu = useCallback(() => setMenu(MenuState.LIMITS), [setMenu])
const closeLimitsMenu = useCallback(() => setMenu(MenuState.DEFAULT), [setMenu])
useEffect(() => {
......@@ -49,11 +49,17 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) {
return
}, [drawerOpen, menu, closeSettings])
useEffect(() => {
if (menu === MenuState.DEFAULT) return // menu is closed, don't log
sendAnalyticsEvent('Portfolio Menu Opened', { name: menu })
}, [menu])
const SubMenu = useMemo(() => {
switch (menu) {
case MenuState.DEFAULT:
return isAuthenticated ? (
<AuthenticatedHeader account={account} openSettings={openSettings} openLimitsMenu={openLimitsMenu} />
<AuthenticatedHeader account={account} openSettings={openSettings} />
) : (
<WalletModal openSettings={openSettings} />
)
......@@ -79,7 +85,6 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) {
isAuthenticated,
menu,
openLanguageSettings,
openLimitsMenu,
openLocalCurrencySettings,
openSettings,
])
......
......@@ -10,7 +10,7 @@ import { useCallback } from 'react'
import { SignatureType } from 'state/signatures/types'
import styled from 'styled-components'
import { EllipsisStyle, ThemedText } from 'theme/components'
import { shortenAddress } from 'utils'
import { shortenAddress } from 'utilities/src/addresses'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PortfolioLogo } from '../PortfolioLogo'
......
import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { CancelLimitsDialog } from 'components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog'
import {
CancelLimitsDialog,
CancellationState,
} from 'components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog'
import { DAI } from 'constants/tokens'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types'
......@@ -29,8 +32,19 @@ const mockOrderDetails: UniswapXOrderDetails = {
offerer: '0x1234',
}
describe('CancelLimitsDialog', () => {
it('should render correctly', () => {
jest.mock('hooks/useTransactionGasFee', () => ({
...jest.requireActual('hooks/useTransactionGasFee'),
useTransactionGasFee: jest.fn(),
}))
jest.mock('components/AccountDrawer/MiniPortfolio/Activity/utils', () => ({
useCreateCancelTransactionRequest: jest.fn(),
}))
// TODO(WEB-3741): figure out why this test is failing locally, but not on CI
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('CancelLimitsDialog', () => {
it('should render correctly', async () => {
const mockOnCancel = jest.fn()
const mockOnConfirm = jest.fn()
render(
......@@ -39,13 +53,15 @@ describe('CancelLimitsDialog', () => {
onConfirm={mockOnConfirm}
isVisible={true}
orders={[mockOrderDetails]}
cancelling={false}
cancelState={CancellationState.REVIEWING_CANCELLATION}
/>
)
expect(document.body).toMatchSnapshot()
expect(
screen.getByText('Are you sure you want to cancel your limit before it executes or expires?')
screen.getByText(
'Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?'
)
).toBeInTheDocument()
})
})
......@@ -4,7 +4,7 @@ import { DetailLineItem, LineItemData } from 'components/swap/DetailLineItem'
import TradePrice from 'components/swap/TradePrice'
import { UniswapXOrderDetails } from 'state/signatures/types'
import { ExternalLink } from 'theme/components'
import { ellipseMiddle } from 'utils/addresses'
import { ellipseMiddle } from 'utilities/src/addresses'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { formatTimestamp } from '../formatTimestamp'
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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