ci(release): publish latest release

parent b7dae665
diff --git a/dist/esm/connectors/injected.js b/dist/esm/connectors/injected.js
index 26f420d68ed9a12deea30a3dca195e2bcf3b3c44..70fc93a7db7b9f4db10e71edd73ee81bd0e28f1e 100644
--- a/dist/esm/connectors/injected.js
+++ b/dist/esm/connectors/injected.js
@@ -405,6 +405,18 @@ export function injected(parameters = {}) {
onChainChanged(chain) {
console.log('[injected] onChainChanged', chain);
const chainId = Number(chain);
+ if (this.id === 'io.metamask')
+ this.getProvider()
+ .then((provider) =>
+ provider
+ ?.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: numberToHex(chainId) }],
+ })
+ .then(() => {})
+ .catch(() => {}),
+ )
+ .catch(() => {})
config.emitter.emit('change', { chainId });
},
async onConnect(connectInfo) {
diff --git a/dist/index.js b/dist/index.js
index f38d06e2de52b5035560d63d6889dc54d5ca021b..4f8fa194b60c73a901dbce4a742f598b1c95c026 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -62,7 +62,7 @@ class CoinbaseWallet extends types_1.Connector {
return __awaiter(this, void 0, void 0, function* () {
if (this.eagerConnection)
return;
- yield (this.eagerConnection = Promise.resolve().then(() => __importStar(require('@coinbase/wallet-sdk'))).then((m) => {
+ yield (this.eagerConnection = Promise.resolve().then(async () => __importStar(await import('@coinbase/wallet-sdk'))).then((m) => {
const _a = this.options, { url } = _a, options = __rest(_a, ["url"]);
this.coinbaseWallet = new m.default(options);
this.provider = this.coinbaseWallet.makeWeb3Provider(url);
diff --git a/dist/index.js b/dist/index.js
index 015a33c37fe87f13f31559d462351acd7ae9bac7..4cd7cdeb4437f30c1c063c0ffb8fd5692a399dbf 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -68,8 +68,8 @@ class GnosisSafe extends types_1.Connector {
if (this.eagerConnection)
return;
// kick off import early to minimize waterfalls
- const SafeAppProviderPromise = Promise.resolve().then(() => __importStar(require('@safe-global/safe-apps-provider'))).then(({ SafeAppProvider }) => SafeAppProvider);
- yield (this.eagerConnection = Promise.resolve().then(() => __importStar(require('@safe-global/safe-apps-sdk'))).then((m) => __awaiter(this, void 0, void 0, function* () {
+ const SafeAppProviderPromise = Promise.resolve().then(async () => __importStar(await import('@safe-global/safe-apps-provider'))).then(({ SafeAppProvider }) => SafeAppProvider);
+ yield (this.eagerConnection = Promise.resolve().then(async () => __importStar(await import('@safe-global/safe-apps-sdk'))).then((m) => __awaiter(this, void 0, void 0, function* () {
this.sdk = new m.default(this.options);
const safe = yield Promise.race([
this.sdk.safe.getInfo(),
diff --git a/dist/index.js b/dist/index.js
index c8476dd9b01c0599dfc545f4c86432081bd0fcec..c0bfce759654232df771724e59a650c92b392f78 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -54,7 +54,7 @@ class MetaMask extends types_1.Connector {
return __awaiter(this, void 0, void 0, function* () {
if (this.eagerConnection)
return;
- return (this.eagerConnection = Promise.resolve().then(() => __importStar(require('@metamask/detect-provider'))).then((m) => __awaiter(this, void 0, void 0, function* () {
+ return (this.eagerConnection = Promise.resolve().then(async () => __importStar(await import('@metamask/detect-provider'))).then((m) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const provider = yield m.default(this.options);
if (provider) {
diff --git a/dist/index.js b/dist/index.js
index 1a36d14c5d7c9ee55b2eccd11216c8adb6839daf..908b8c57a2d8cd565030e34e15c56caf7d182cfd 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -84,7 +84,7 @@ class WalletConnect extends types_1.Connector {
return __awaiter(this, void 0, void 0, function* () {
const rpcMap = this.rpcMap ? (0, utils_1.getBestUrlMap)(this.rpcMap, this.timeout) : undefined;
const chainProps = this.getChainProps(this.chains, this.optionalChains, desiredChainId);
- const ethProviderModule = yield Promise.resolve().then(() => __importStar(require('@walletconnect/ethereum-provider')));
+ const ethProviderModule = yield Promise.resolve().then(async () => __importStar(await import('@walletconnect/ethereum-provider')));
this.provider = yield ethProviderModule.default.init(Object.assign(Object.assign(Object.assign({}, this.options), chainProps), { rpcMap: yield rpcMap }));
return this.provider
.on('disconnect', this.disconnectListener)
diff --git a/dist/utils.js b/dist/utils.js
index 17539b6f910e65aeaebcc116395dce56ae4ce193..9ea637118844ebbdc55db71009b44849992603d2 100644
--- a/dist/utils.js
+++ b/dist/utils.js
@@ -62,8 +62,8 @@ function getBestUrl(urls, timeout) {
if (urls.length === 1)
return urls[0];
const [HttpConnection, JsonRpcProvider] = yield Promise.all([
- Promise.resolve().then(() => __importStar(require('@walletconnect/jsonrpc-http-connection'))).then(({ HttpConnection }) => HttpConnection),
- Promise.resolve().then(() => __importStar(require('@walletconnect/jsonrpc-provider'))).then(({ JsonRpcProvider }) => JsonRpcProvider),
+ Promise.resolve().then(async () => __importStar(await import('@walletconnect/jsonrpc-http-connection'))).then(({ HttpConnection }) => HttpConnection),
+ Promise.resolve().then(async () => __importStar(await import('@walletconnect/jsonrpc-provider'))).then(({ JsonRpcProvider }) => JsonRpcProvider),
]);
// the below returns the first url for which there's been a successful call, prioritized by index
return new Promise((resolve) => {
diff --git a/lib/browser/eip1193.js b/lib/browser/eip1193.js
index ce028c25a164d8af8d513bc0eae4cf104234f6e8..81ba2f29d379f18c04c57b5a560cc6c4f4b57e0d 100644
--- a/lib/browser/eip1193.js
+++ b/lib/browser/eip1193.js
@@ -70,6 +70,7 @@ class Eip1193 extends eip1193_bridge_1.Eip1193Bridge {
yield _super.send.call(this, method, params);
// Providers will not "rewind" to an older block number nor notice chain changes, so they must be reset.
this.utils.providers.forEach((provider) => provider.reset());
+ this.emit('chainChanged', params[0].chainId);
break;
default:
result = yield _super.send.call(this, method, params);
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmYVT8bXNHTgPdSTegLQ62Z6f7E51fQbeMvn2nJAxMRGBE` - CIDv0: `QmP8VRKK9FDri6XhqC8xNCXJTRqbfWmiF7jTdaDBJbt3uv`
- CIDv1: `bafybeiew2yawtry3lld6g5ankyq3bsbvjfchxxhboph45u6os6t2babtru` - CIDv1: `bafybeialxy3kyi4xjtgo4u3a6k7gpt76j3y77m3cucofzbjxlegxfzufpu`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
...@@ -10,15 +10,52 @@ You can also access the Uniswap Interface from an IPFS gateway. ...@@ -10,15 +10,52 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeiew2yawtry3lld6g5ankyq3bsbvjfchxxhboph45u6os6t2babtru.ipfs.dweb.link/ - https://bafybeialxy3kyi4xjtgo4u3a6k7gpt76j3y77m3cucofzbjxlegxfzufpu.ipfs.dweb.link/
- https://bafybeiew2yawtry3lld6g5ankyq3bsbvjfchxxhboph45u6os6t2babtru.ipfs.cf-ipfs.com/ - https://bafybeialxy3kyi4xjtgo4u3a6k7gpt76j3y77m3cucofzbjxlegxfzufpu.ipfs.cf-ipfs.com/
- [ipfs://QmYVT8bXNHTgPdSTegLQ62Z6f7E51fQbeMvn2nJAxMRGBE/](ipfs://QmYVT8bXNHTgPdSTegLQ62Z6f7E51fQbeMvn2nJAxMRGBE/) - [ipfs://QmP8VRKK9FDri6XhqC8xNCXJTRqbfWmiF7jTdaDBJbt3uv/](ipfs://QmP8VRKK9FDri6XhqC8xNCXJTRqbfWmiF7jTdaDBJbt3uv/)
### 5.62.4 (2024-12-16) ## 5.63.0 (2024-12-17)
### Features
* **web:** add indicators for % difference from current price (#14235) 6456766
* **web:** add more interactivity to range input price chart (#14153) f9c4680
* **web:** filter v2 unsupported chains from LP creation flow (#14462) 734a4e2
* **web:** mweb designs for price range input (#14424) 2277e82
* **web:** pool finder redesign and re-enable on new LP pages (#14451) d55c7ea
* **web:** Revise "unavailable" state for small price charts (#14311) aba0989
### Bug Fixes ### Bug Fixes
* **web:** fix disabled swap button for previously-dismissed warning tokens (#14560) 2b04e71 * **web:** 12 16 fix web add monad testnet rpc to web env staging (#14564) 588d8bb
* **web:** check window.__DEV__ cypress fix (#14491) a885720
* **web:** cherry-pick pagination into staging (#14551) 3920adf
* **web:** cleanup unused legacy FOR modal (#14356) 302b4f3
* **web:** Conversion API updates (#14550) 242f8da
* **web:** downgrade react-native-web to 0.19.10 (#14473) 010b773
* **web:** enforce privacy opt out choices (#14374) 9760922
* **web:** fix broken link for providing lps (#14372) 7c06390
* **web:** fix current price inversion issue (#14445) 3fe1286
* **web:** fix disabled swap button for previously-dismissed warning tokens (#14559) 4868b63
* **web:** fix missing mweb swap - staging (#14580) d8f6631
* **web:** fix v2 lp create networks (#14578) ef585ef
* **web:** hide un-owned positions (#14447) 742e519
* **web:** hiding migrate to v4 (#14499) 98825bb
* **web:** lp links open in new tabs - staging (#14571) bdc1de5
* **web:** modal diet - part ii (#14364) 5e22130
* **web:** prevent crash when sending on bnb chain (#14355) b47fc90
* **web:** price range input - prevent scrolling below zero (#14316) e4e37b4
* **web:** staging cherrypicks - tooltip + position page crash (#14581) d94f7f9
* **web:** surface imported v2 positions (#14405) a3519b4
* **web:** truncation issue on mad price text positions (#14582) 78038ed
* **web:** update the create flow to get data from the sdk instead of from the backend (#14380) 50ab440
* **web:** wrap positions in multichain context (#14466) bc07af4
### Continuous Integration
* **web:** update sitemaps 721fc2d
web/5.62.4 web/5.63.0
\ No newline at end of file \ No newline at end of file
ignores: [ ignores: [
# Dependencies that depcheck thinks are unused but are actually used # Dependencies that depcheck thinks are unused but are actually used
"react-native-web", 'react-native-web',
"jest-environment-jsdom", 'jest-environment-jsdom',
"webpack-cli", 'webpack-cli',
# Dependencies that depcheck thinks are missing but are actually present or never used # Dependencies that depcheck thinks are missing but are actually present or never used
## Internal packages / workspaces ## Internal packages / workspaces
"src", 'src',
"tsconfig", 'tsconfig',
# Webpack plugins # Webpack plugins
"@svgr/webpack", '@svgr/webpack',
"tamagui-loader", 'tamagui-loader',
"esbuild-loader", 'esbuild-loader',
"swc-loader", 'style-loader',
'css-loader',
'swc-loader',
## Testing ## Testing
"@testing-library/dom", '@testing-library/dom',
] ]
...@@ -67,6 +67,7 @@ ...@@ -67,6 +67,7 @@
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
"concurrently": "8.2.2", "concurrently": "8.2.2",
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "11.0.0",
"css-loader": "6.11.0",
"esbuild-loader": "3.2.0", "esbuild-loader": "3.2.0",
"eslint": "8.44.0", "eslint": "8.44.0",
"jest": "29.7.0", "jest": "29.7.0",
...@@ -77,6 +78,7 @@ ...@@ -77,6 +78,7 @@
"react-refresh": "0.14.0", "react-refresh": "0.14.0",
"serve": "14.2.4", "serve": "14.2.4",
"statsig-js": "4.41.0", "statsig-js": "4.41.0",
"style-loader": "3.3.2",
"swc-loader": "0.2.6", "swc-loader": "0.2.6",
"tamagui-loader": "1.114.4", "tamagui-loader": "1.114.4",
"typescript": "5.3.3", "typescript": "5.3.3",
......
...@@ -12,7 +12,7 @@ export async function initializeDatadog(appName: string): Promise<void> { ...@@ -12,7 +12,7 @@ export async function initializeDatadog(appName: string): Promise<void> {
const datadogEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Datadog)) const datadogEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Datadog))
logger.setWalletDatadogEnabled(datadogEnabled) logger.setWalletDatadogEnabled(datadogEnabled)
if (__DEV__ || !datadogEnabled) { if (!datadogEnabled) {
return return
} }
......
...@@ -5,7 +5,7 @@ import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice' ...@@ -5,7 +5,7 @@ import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { useGasFeeFormattedAmounts, useTransactionGasFee } from 'uniswap/src/features/gas/hooks' import { useGasFeeFormattedDisplayAmounts, useTransactionGasFee } from 'uniswap/src/features/gas/hooks'
import { useActiveAccountAddressWithThrow, useDisplayName } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddressWithThrow, useDisplayName } from 'wallet/src/features/wallet/hooks'
export const WrapTransactionDetails = ({ export const WrapTransactionDetails = ({
...@@ -31,7 +31,7 @@ export const WrapTransactionDetails = ({ ...@@ -31,7 +31,7 @@ export const WrapTransactionDetails = ({
const networkFee = useTransactionGasFee(txRequest) const networkFee = useTransactionGasFee(txRequest)
const { gasFeeFormatted } = useGasFeeFormattedAmounts({ const { gasFeeFormatted } = useGasFeeFormattedDisplayAmounts({
gasFee: networkFee, gasFee: networkFee,
chainId, chainId,
placeholder: undefined, placeholder: undefined,
......
...@@ -92,6 +92,7 @@ export function SettingsScreen(): JSX.Element { ...@@ -92,6 +92,7 @@ export function SettingsScreen(): JSX.Element {
const fireAnalytic = (): void => { const fireAnalytic = (): void => {
sendAnalyticsEvent(WalletEventName.TestnetModeToggled, { sendAnalyticsEvent(WalletEventName.TestnetModeToggled, {
enabled: isChecked, enabled: isChecked,
location: 'settings',
}) })
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Uniswap Extension", "name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.12.0", "version": "1.13.0",
"minimum_chrome_version": "116", "minimum_chrome_version": "116",
"icons": { "icons": {
"16": "assets/icon16.png", "16": "assets/icon16.png",
......
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
v14Schema, v14Schema,
v15Schema, v15Schema,
v16Schema, v16Schema,
v17Schema,
v1Schema, v1Schema,
v2Schema, v2Schema,
v3Schema, v3Schema,
...@@ -48,6 +49,7 @@ import { ...@@ -48,6 +49,7 @@ import {
testMovedUserSettings, testMovedUserSettings,
testRemoveCreatedOnboardingRedesignAccount, testRemoveCreatedOnboardingRedesignAccount,
testRemoveHoldToSwap, testRemoveHoldToSwap,
testUnchecksumDismissedTokenWarningKeys,
testUpdateExploreOrderByType, testUpdateExploreOrderByType,
} from 'wallet/src/state/walletMigrationsTests' } from 'wallet/src/state/walletMigrationsTests'
...@@ -274,4 +276,8 @@ describe('Redux state migrations', () => { ...@@ -274,4 +276,8 @@ describe('Redux state migrations', () => {
it('migrates from v16 to v17', async () => { it('migrates from v16 to v17', async () => {
testRemoveCreatedOnboardingRedesignAccount(migrations[17], v16Schema) testRemoveCreatedOnboardingRedesignAccount(migrations[17], v16Schema)
}) })
it('migrates from v17 to v18', () => {
testUnchecksumDismissedTokenWarningKeys(migrations[18], v17Schema)
})
}) })
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { unchecksumDismissedTokenWarningKeys } from 'uniswap/src/state/uniswapMigrations'
import { import {
activatePendingAccounts, activatePendingAccounts,
addCreatedOnboardingRedesignAccountBehaviorHistory, addCreatedOnboardingRedesignAccountBehaviorHistory,
...@@ -42,6 +43,7 @@ export const migrations = { ...@@ -42,6 +43,7 @@ export const migrations = {
15: moveCurrencySetting, 15: moveCurrencySetting,
16: updateExploreOrderByType, 16: updateExploreOrderByType,
17: removeCreatedOnboardingRedesignAccountBehaviorHistory, 17: removeCreatedOnboardingRedesignAccountBehaviorHistory,
18: unchecksumDismissedTokenWarningKeys,
} }
export const EXTENSION_STATE_VERSION = 17 export const EXTENSION_STATE_VERSION = 18
...@@ -31,6 +31,19 @@ const normalizedStories = [ ...@@ -31,6 +31,19 @@ const normalizedStories = [
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/ /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/
), ),
}, },
{
titlePrefix: "",
directory: "../../packages/ui/src",
files: "**/*.mdx",
importPathMatcher:
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.mdx)$/,
// @ts-ignore
req: require.context(
"../../../packages/ui/src",
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.mdx)$/
),
},
]; ];
declare global { declare global {
......
...@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) { ...@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
} }
def devVersionName = "1.42" def devVersionName = "1.43"
def betaVersionName = "1.42" def betaVersionName = "1.43"
def prodVersionName = "1.42" def prodVersionName = "1.43"
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
......
...@@ -2204,7 +2204,7 @@ ...@@ -2204,7 +2204,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2257,7 +2257,7 @@ ...@@ -2257,7 +2257,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2310,7 +2310,7 @@ ...@@ -2310,7 +2310,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2363,7 +2363,7 @@ ...@@ -2363,7 +2363,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2401,7 +2401,7 @@ ...@@ -2401,7 +2401,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2437,7 +2437,7 @@ ...@@ -2437,7 +2437,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2472,7 +2472,7 @@ ...@@ -2472,7 +2472,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2507,7 +2507,7 @@ ...@@ -2507,7 +2507,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2554,7 +2554,7 @@ ...@@ -2554,7 +2554,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2600,7 +2600,7 @@ ...@@ -2600,7 +2600,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
...@@ -2646,7 +2646,7 @@ ...@@ -2646,7 +2646,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
...@@ -2692,7 +2692,7 @@ ...@@ -2692,7 +2692,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
...@@ -2734,7 +2734,7 @@ ...@@ -2734,7 +2734,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2777,7 +2777,7 @@ ...@@ -2777,7 +2777,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
...@@ -2820,7 +2820,7 @@ ...@@ -2820,7 +2820,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
...@@ -2863,7 +2863,7 @@ ...@@ -2863,7 +2863,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
...@@ -2899,7 +2899,7 @@ ...@@ -2899,7 +2899,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -2937,7 +2937,7 @@ ...@@ -2937,7 +2937,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3137,7 +3137,7 @@ ...@@ -3137,7 +3137,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -3181,7 +3181,7 @@ ...@@ -3181,7 +3181,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
...@@ -3292,7 +3292,7 @@ ...@@ -3292,7 +3292,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3363,7 +3363,7 @@ ...@@ -3363,7 +3363,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
...@@ -3474,7 +3474,7 @@ ...@@ -3474,7 +3474,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3545,7 +3545,7 @@ ...@@ -3545,7 +3545,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.42; MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
"firestore:deploy:rules": "firebase deploy --only firestore:rules", "firestore:deploy:rules": "firebase deploy --only firestore:rules",
"link:assets": "react-native-asset", "link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate", "graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 8", "check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 2",
"ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios", "ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios",
"ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift", "ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift",
"ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"", "ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"",
...@@ -87,11 +87,12 @@ ...@@ -87,11 +87,12 @@
"@shopify/react-native-performance-navigation": "3.0.0", "@shopify/react-native-performance-navigation": "3.0.0",
"@shopify/react-native-skia": "1.4.2", "@shopify/react-native-skia": "1.4.2",
"@sparkfabrik/react-native-idfa-aaid": "1.2.0", "@sparkfabrik/react-native-idfa-aaid": "1.2.0",
"@tanstack/react-query": "5.51.16",
"@uniswap/analytics": "1.7.0", "@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.40.0", "@uniswap/analytics-events": "2.40.0",
"@uniswap/client-explore": "0.0.12", "@uniswap/client-explore": "0.0.12",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "6.0.0", "@uniswap/sdk-core": "6.1.0",
"@walletconnect/core": "2.17.1", "@walletconnect/core": "2.17.1",
"@walletconnect/react-native-compat": "2.17.1", "@walletconnect/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1", "@walletconnect/utils": "2.17.1",
......
...@@ -15,7 +15,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context' ...@@ -15,7 +15,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens' import { enableFreeze } from 'react-native-screens'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { DatadogProviderWrapper } from 'src/app/DataDogProvider' import { DatadogProviderWrapper } from 'src/app/DatadogProviderWrapper'
import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider'
import { AppModals } from 'src/app/modals/AppModals' import { AppModals } from 'src/app/modals/AppModals'
import { NavigationContainer } from 'src/app/navigation/NavigationContainer' import { NavigationContainer } from 'src/app/navigation/NavigationContainer'
......
import {
BatchSize,
DatadogProvider,
DatadogProviderConfiguration,
SdkVerbosity,
TrackingConsent,
UploadFrequency,
} from '@datadog/mobile-react-native'
import { ErrorEventMapper } from '@datadog/mobile-react-native/lib/typescript/rum/eventMappers/errorEventMapper'
import { PropsWithChildren, default as React } from 'react'
import { getDatadogEnvironment } from 'src/utils/version'
import { config } from 'uniswap/src/config'
import { isDetoxBuild, isJestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger'
const ENABLE_DATADOG = localDevDatadogEnabled || !__DEV__
const datadogConfig = new DatadogProviderConfiguration(
config.datadogClientToken,
getDatadogEnvironment(),
config.datadogProjectId,
ENABLE_DATADOG, // trackInteractions
ENABLE_DATADOG, // trackResources
ENABLE_DATADOG, // trackErrors
localDevDatadogEnabled ? TrackingConsent.GRANTED : undefined,
)
Object.assign(datadogConfig, {
site: 'US1',
longTaskThresholdMs: 100,
nativeCrashReportEnabled: true,
verbosity: SdkVerbosity.INFO,
errorEventMapper: (event: ReturnType<ErrorEventMapper>) => {
// this is Sentry error, which is caused by the not complete closing of their SDK
if (event?.message.includes('Native is disabled')) {
return null
}
return event
},
})
if (localDevDatadogEnabled) {
Object.assign(datadogConfig, {
sessionSamplingRate: 100,
resourceTracingSamplingRate: 100,
uploadFrequency: UploadFrequency.FREQUENT,
batchSize: BatchSize.SMALL,
verbosity: SdkVerbosity.DEBUG,
})
}
/**
* Wrapper component to provide Datadog to the app with our mobile app's
* specific configuration.
*/
export function DatadogProviderWrapper({ children }: PropsWithChildren): JSX.Element {
logger.setWalletDatadogEnabled(true)
if (isDetoxBuild || isJestRun) {
return <>{children}</>
}
return <DatadogProvider configuration={datadogConfig}>{children}</DatadogProvider>
}
...@@ -10,18 +10,16 @@ import { ErrorEventMapper } from '@datadog/mobile-react-native/lib/typescript/ru ...@@ -10,18 +10,16 @@ import { ErrorEventMapper } from '@datadog/mobile-react-native/lib/typescript/ru
import { PropsWithChildren, default as React } from 'react' import { PropsWithChildren, default as React } from 'react'
import { getDatadogEnvironment } from 'src/utils/version' import { getDatadogEnvironment } from 'src/utils/version'
import { config } from 'uniswap/src/config' import { config } from 'uniswap/src/config'
import { isDetoxBuild, isJestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants' import { datadogEnabled, isDetoxBuild, isJestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
const ENABLE_DATADOG = localDevDatadogEnabled || !__DEV__
const datadogConfig = new DatadogProviderConfiguration( const datadogConfig = new DatadogProviderConfiguration(
config.datadogClientToken, config.datadogClientToken,
getDatadogEnvironment(), getDatadogEnvironment(),
config.datadogProjectId, config.datadogProjectId,
ENABLE_DATADOG, // trackInteractions datadogEnabled, // trackInteractions
ENABLE_DATADOG, // trackResources datadogEnabled, // trackResources
ENABLE_DATADOG, // trackErrors datadogEnabled, // trackErrors
localDevDatadogEnabled ? TrackingConsent.GRANTED : undefined, localDevDatadogEnabled ? TrackingConsent.GRANTED : undefined,
) )
......
...@@ -83,6 +83,7 @@ import { ...@@ -83,6 +83,7 @@ import {
v79Schema, v79Schema,
v7Schema, v7Schema,
v80Schema, v80Schema,
v81Schema,
v8Schema, v8Schema,
v9Schema, v9Schema,
} from 'src/app/schema' } from 'src/app/schema'
...@@ -125,6 +126,7 @@ import { ...@@ -125,6 +126,7 @@ import {
testMovedUserSettings, testMovedUserSettings,
testRemoveCreatedOnboardingRedesignAccount, testRemoveCreatedOnboardingRedesignAccount,
testRemoveHoldToSwap, testRemoveHoldToSwap,
testUnchecksumDismissedTokenWarningKeys,
testUpdateExploreOrderByType, testUpdateExploreOrderByType,
} from 'wallet/src/state/walletMigrationsTests' } from 'wallet/src/state/walletMigrationsTests'
import { signerMnemonicAccount } from 'wallet/src/test/fixtures' import { signerMnemonicAccount } from 'wallet/src/test/fixtures'
...@@ -1592,4 +1594,8 @@ describe('Redux state migrations', () => { ...@@ -1592,4 +1594,8 @@ describe('Redux state migrations', () => {
it('migrates from v80 to v81', async () => { it('migrates from v80 to v81', async () => {
testRemoveCreatedOnboardingRedesignAccount(migrations[81], v80Schema) testRemoveCreatedOnboardingRedesignAccount(migrations[81], v80Schema)
}) })
it('migrates from v81 to v82', () => {
testUnchecksumDismissedTokenWarningKeys(migrations[82], v81Schema)
})
}) })
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
TransactionStatus, TransactionStatus,
TransactionType, TransactionType,
} from 'uniswap/src/features/transactions/types/transactionDetails' } from 'uniswap/src/features/transactions/types/transactionDetails'
import { unchecksumDismissedTokenWarningKeys } from 'uniswap/src/state/uniswapMigrations'
import { getNFTAssetKey } from 'wallet/src/features/nfts/utils' import { getNFTAssetKey } from 'wallet/src/features/nfts/utils'
import { Account } from 'wallet/src/features/wallet/accounts/types' import { Account } from 'wallet/src/features/wallet/accounts/types'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
...@@ -954,6 +955,8 @@ export const migrations = { ...@@ -954,6 +955,8 @@ export const migrations = {
80: updateExploreOrderByType, 80: updateExploreOrderByType,
81: removeCreatedOnboardingRedesignAccountBehaviorHistory, 81: removeCreatedOnboardingRedesignAccountBehaviorHistory,
82: unchecksumDismissedTokenWarningKeys,
} }
export const MOBILE_STATE_VERSION = 81 export const MOBILE_STATE_VERSION = 82
...@@ -21,6 +21,7 @@ import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferM ...@@ -21,6 +21,7 @@ import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferM
import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal' import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal'
import { closeModal } from 'src/features/modals/modalSlice' import { closeModal } from 'src/features/modals/modalSlice'
import { ScantasticModal } from 'src/features/scantastic/ScantasticModal' import { ScantasticModal } from 'src/features/scantastic/ScantasticModal'
import { TestnetSwitchModal } from 'src/features/testnetMode/TestnetSwitchModal'
import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal' import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal'
import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal' import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
...@@ -119,6 +120,10 @@ export function AppModals(): JSX.Element { ...@@ -119,6 +120,10 @@ export function AppModals(): JSX.Element {
<LazyModalRenderer name={ModalName.TokenWarning}> <LazyModalRenderer name={ModalName.TokenWarning}>
<TokenWarningModalWrapper /> <TokenWarningModalWrapper />
</LazyModalRenderer> </LazyModalRenderer>
<LazyModalRenderer name={ModalName.TestnetSwitchModal}>
<TestnetSwitchModal />
</LazyModalRenderer>
</> </>
) )
} }
import { DdRumReactNavigationTracking } from '@datadog/mobile-react-navigation' import { DdRumReactNavigationTracking } from '@datadog/mobile-react-navigation'
import { import {
createNavigationContainerRef,
DefaultTheme, DefaultTheme,
NavigationContainer as NativeNavigationContainer, NavigationContainer as NativeNavigationContainer,
NavigationContainerRefWithCurrent, NavigationContainerRefWithCurrent,
...@@ -9,6 +8,7 @@ import { SharedEventName } from '@uniswap/analytics-events' ...@@ -9,6 +8,7 @@ import { SharedEventName } from '@uniswap/analytics-events'
import React, { FC, PropsWithChildren, useCallback, useState } from 'react' import React, { FC, PropsWithChildren, useCallback, useState } from 'react'
import { Linking } from 'react-native' import { Linking } from 'react-native'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { navigationRef } from 'src/app/navigation/navigationRef'
import { RootParamList } from 'src/app/navigation/types' import { RootParamList } from 'src/app/navigation/types'
import { openDeepLink } from 'src/features/deepLinking/handleDeepLinkSaga' import { openDeepLink } from 'src/features/deepLinking/handleDeepLinkSaga'
import { DIRECT_LOG_ONLY_SCREENS } from 'src/features/telemetry/directLogScreens' import { DIRECT_LOG_ONLY_SCREENS } from 'src/features/telemetry/directLogScreens'
...@@ -18,6 +18,7 @@ import { useSporeColors } from 'ui/src' ...@@ -18,6 +18,7 @@ import { useSporeColors } from 'ui/src'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileNavScreen } from 'uniswap/src/types/screens/mobile' import { MobileNavScreen } from 'uniswap/src/types/screens/mobile'
import { datadogEnabled } from 'utilities/src/environment/constants'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
import { sleep } from 'utilities/src/time/timing' import { sleep } from 'utilities/src/time/timing'
...@@ -25,8 +26,6 @@ interface Props { ...@@ -25,8 +26,6 @@ interface Props {
onReady?: (navigationRef: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>) => void onReady?: (navigationRef: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>) => void
} }
export const navigationRef = createNavigationContainerRef()
/** Wrapped `NavigationContainer` with telemetry tracing. */ /** Wrapped `NavigationContainer` with telemetry tracing. */
export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, onReady }: PropsWithChildren<Props>) => { export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, onReady }: PropsWithChildren<Props>) => {
const colors = useSporeColors() const colors = useSporeColors()
...@@ -54,7 +53,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on ...@@ -54,7 +53,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
const initialRoute = navigationRef.getCurrentRoute()?.name as MobileNavScreen const initialRoute = navigationRef.getCurrentRoute()?.name as MobileNavScreen
setRouteName(initialRoute) setRouteName(initialRoute)
if (!__DEV__) { if (datadogEnabled) {
DdRumReactNavigationTracking.startTrackingViews(navigationRef.current) DdRumReactNavigationTracking.startTrackingViews(navigationRef.current)
} }
}} }}
......
import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native' import { DdRumReactNavigationTracking } from '@datadog/mobile-react-navigation'
import {
NavigationContainer,
NavigationContainerRefWithCurrent,
NavigationState,
createNavigationContainerRef,
} from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack' import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { TransitionPresets, createStackNavigator } from '@react-navigation/stack' import { TransitionPresets, createStackNavigator } from '@react-navigation/stack'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { DevSettings } from 'react-native' import { DevSettings } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import StorybookUIRoot from 'src/../.storybook' import StorybookUIRoot from 'src/../.storybook'
import { navigationRef } from 'src/app/navigation/NavigationContainer'
import { renderHeaderBackButton, renderHeaderBackImage } from 'src/app/navigation/components' import { renderHeaderBackButton, renderHeaderBackImage } from 'src/app/navigation/components'
import { navigationRef } from 'src/app/navigation/navigationRef'
import { import {
AppStackParamList, AppStackParamList,
AppStackScreenProp, AppStackScreenProp,
...@@ -79,6 +85,7 @@ import { ...@@ -79,6 +85,7 @@ import {
UnitagScreens, UnitagScreens,
UnitagStackParamList, UnitagStackParamList,
} from 'uniswap/src/types/screens/mobile' } from 'uniswap/src/types/screens/mobile'
import { datadogEnabled } from 'utilities/src/environment/constants'
import { isDevEnv } from 'utilities/src/environment/env' import { isDevEnv } from 'utilities/src/environment/env'
import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext' import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
...@@ -138,6 +145,45 @@ export function WrappedHomeScreen(props: AppStackScreenProp<MobileScreens.Home>) ...@@ -138,6 +145,45 @@ export function WrappedHomeScreen(props: AppStackScreenProp<MobileScreens.Home>)
} }
export const exploreNavigationRef = createNavigationContainerRef<ExploreStackParamList>() export const exploreNavigationRef = createNavigationContainerRef<ExploreStackParamList>()
const fiatOnRampNavigationRef = createNavigationContainerRef<FiatOnRampStackParamList>()
const navRefs = [exploreNavigationRef, fiatOnRampNavigationRef, navigationRef]
/**
* Since we are using multiple navigation containers, we need to start and stop tracking views
* manually since multiple nav containers are not supported by the Datadog RUM.
*
* https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/integrated_libraries/reactnative/#track-view-navigation
*/
const startTracking = (
navRefToStartTracking: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>,
): void => {
if (!datadogEnabled) {
return
}
navRefs.forEach((navRef) => {
DdRumReactNavigationTracking.stopTrackingViews(navRef.current)
})
DdRumReactNavigationTracking.startTrackingViews(navRefToStartTracking.current)
}
/**
* Since we are using multiple navigation containers, we need to start and stop tracking views
* manually since multiple nav containers are not supported by the Datadog RUM.
*
* https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/integrated_libraries/reactnative/#track-view-navigation
*/
const stopTracking = (state: NavigationState | undefined): void => {
if (!datadogEnabled) {
return
}
const navContainerIsClosing = !state || state.routes.length === 0
if (navContainerIsClosing) {
navRefs.forEach((navRef) => {
DdRumReactNavigationTracking.stopTrackingViews(navRef.current)
})
DdRumReactNavigationTracking.startTrackingViews(navigationRef.current)
}
}
export function ExploreStackNavigator(): JSX.Element { export function ExploreStackNavigator(): JSX.Element {
const colors = useSporeColors() const colors = useSporeColors()
...@@ -145,7 +191,7 @@ export function ExploreStackNavigator(): JSX.Element { ...@@ -145,7 +191,7 @@ export function ExploreStackNavigator(): JSX.Element {
return ( return (
<NavigationContainer <NavigationContainer
ref={exploreNavigationRef} ref={exploreNavigationRef}
independent={true} independent
theme={{ theme={{
dark: false, dark: false,
colors: { colors: {
...@@ -157,6 +203,8 @@ export function ExploreStackNavigator(): JSX.Element { ...@@ -157,6 +203,8 @@ export function ExploreStackNavigator(): JSX.Element {
notification: 'transparent', notification: 'transparent',
}, },
}} }}
onStateChange={stopTracking}
onReady={() => startTracking(exploreNavigationRef)}
> >
<HorizontalEdgeGestureTarget /> <HorizontalEdgeGestureTarget />
<ExploreStack.Navigator <ExploreStack.Navigator
...@@ -186,7 +234,12 @@ export function ExploreStackNavigator(): JSX.Element { ...@@ -186,7 +234,12 @@ export function ExploreStackNavigator(): JSX.Element {
export function FiatOnRampStackNavigator(): JSX.Element { export function FiatOnRampStackNavigator(): JSX.Element {
return ( return (
<NavigationContainer independent={true}> <NavigationContainer
ref={fiatOnRampNavigationRef}
independent
onReady={() => startTracking(fiatOnRampNavigationRef)}
onStateChange={stopTracking}
>
<HorizontalEdgeGestureTarget /> <HorizontalEdgeGestureTarget />
<FiatOnRampProvider> <FiatOnRampProvider>
<FiatOnRampStack.Navigator <FiatOnRampStack.Navigator
......
import { createNavigationContainerRef } from '@react-navigation/native'
// this was moved to its own file to avoid circular dependencies
export const navigationRef = createNavigationContainerRef()
import { NavigationAction, NavigationState } from '@react-navigation/core' import { NavigationAction, NavigationState } from '@react-navigation/core'
import { navigationRef } from 'src/app/navigation/NavigationContainer' import { navigationRef } from 'src/app/navigation/navigationRef'
import { RootParamList } from 'src/app/navigation/types' import { RootParamList } from 'src/app/navigation/types'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
......
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { ScrollView, StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
...@@ -12,6 +13,11 @@ import { Account } from 'wallet/src/features/wallet/accounts/types' ...@@ -12,6 +13,11 @@ import { Account } from 'wallet/src/features/wallet/accounts/types'
const ADDRESS_ROW_HEIGHT = 40 const ADDRESS_ROW_HEIGHT = 40
interface SortedAddressData {
address: string
balance: number
}
type Portfolio = NonNullable<NonNullable<NonNullable<AccountListQuery['portfolios']>[0]>> type Portfolio = NonNullable<NonNullable<NonNullable<AccountListQuery['portfolios']>[0]>>
function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element { function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element {
...@@ -26,17 +32,27 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele ...@@ -26,17 +32,27 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele
.filter((portfolio): portfolio is Portfolio => Boolean(portfolio)) .filter((portfolio): portfolio is Portfolio => Boolean(portfolio))
.map((portfolio) => ({ .map((portfolio) => ({
address: portfolio.ownerAddress, address: portfolio.ownerAddress,
balance: portfolio.tokensTotalDenominatedValue?.value, balance: portfolio.tokensTotalDenominatedValue?.value ?? 0,
})) }))
.sort((a, b) => (b.balance ?? 0) - (a.balance ?? 0)) .sort((a, b) => b.balance - a.balance)
// set max height to around 30% screen size, so we always cut the last visible element
// this way user is aware if there are more elements to see
const accountsScrollViewHeight = const accountsScrollViewHeight =
Math.floor((fullHeight * 0.3) / ADDRESS_ROW_HEIGHT) * ADDRESS_ROW_HEIGHT + Math.floor((fullHeight * 0.3) / ADDRESS_ROW_HEIGHT) * ADDRESS_ROW_HEIGHT +
ADDRESS_ROW_HEIGHT / 2 + ADDRESS_ROW_HEIGHT / 2 +
spacing.spacing12 // 12 is the ScrollView vertical padding spacing.spacing12 // 12 is the ScrollView vertical padding
const renderItem = ({ item, index }: { item: SortedAddressData; index: number }): JSX.Element => {
return (
<AssociatedAccountRow
address={item.address}
balance={item.balance}
index={index}
loading={loading}
totalCount={accounts.length}
/>
)
}
return ( return (
<Flex <Flex
borderColor="$surface3" borderColor="$surface3"
...@@ -46,17 +62,14 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele ...@@ -46,17 +62,14 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele
px="$spacing12" px="$spacing12"
width="100%" width="100%"
> >
<ScrollView bounces={false} contentContainerStyle={styles.accounts}> <FlatList
{sortedAddressesByBalance.map(({ address, balance }, index) => ( data={sortedAddressesByBalance}
<AssociatedAccountRow keyExtractor={(item) => item.address}
address={address} renderItem={renderItem}
balance={balance} bounces={false}
index={index} contentContainerStyle={[styles.accounts, { paddingBottom: spacing.spacing12 }]}
loading={loading} keyboardShouldPersistTaps="handled"
totalCount={accounts.length} />
/>
))}
</ScrollView>
</Flex> </Flex>
) )
} }
......
...@@ -2,7 +2,7 @@ import * as ExpoClipboard from 'expo-clipboard' ...@@ -2,7 +2,7 @@ import * as ExpoClipboard from 'expo-clipboard'
import { State } from 'react-native-gesture-handler' import { State } from 'react-native-gesture-handler'
import { fireGestureHandler, getByGestureTestId } from 'react-native-gesture-handler/jest-utils' import { fireGestureHandler, getByGestureTestId } from 'react-native-gesture-handler/jest-utils'
import { MobileState } from 'src/app/mobileReducer' import { MobileState } from 'src/app/mobileReducer'
import { navigationRef } from 'src/app/navigation/NavigationContainer' import { navigationRef } from 'src/app/navigation/navigationRef'
import { AccountHeader } from 'src/components/accounts/AccountHeader' import { AccountHeader } from 'src/components/accounts/AccountHeader'
import { fireEvent, render, screen, waitFor, within } from 'src/test/test-utils' import { fireEvent, render, screen, waitFor, within } from 'src/test/test-utils'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
......
...@@ -5,8 +5,9 @@ import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTim ...@@ -5,8 +5,9 @@ import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTim
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, TouchableArea } from 'ui/src' import { Flex, Text, TouchableArea } from 'ui/src'
import { CopyAlt, Settings } from 'ui/src/components/icons' import { CopyAlt, ScanHome, Settings } from 'ui/src/components/icons'
import { AccountType } from 'uniswap/src/features/accounts/types' import { AccountType } from 'uniswap/src/features/accounts/types'
import { useAvatar } from 'uniswap/src/features/address/avatar' import { useAvatar } from 'uniswap/src/features/address/avatar'
import { pushNotification } from 'uniswap/src/features/notifications/slice' import { pushNotification } from 'uniswap/src/features/notifications/slice'
...@@ -20,12 +21,16 @@ import { sanitizeAddressText } from 'uniswap/src/utils/addresses' ...@@ -20,12 +21,16 @@ import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { setClipboard } from 'uniswap/src/utils/clipboard' import { setClipboard } from 'uniswap/src/utils/clipboard'
import { shortenAddress } from 'utilities/src/addresses' import { shortenAddress } from 'utilities/src/addresses'
import { isDevEnv } from 'utilities/src/environment/env' import { isDevEnv } from 'utilities/src/environment/env'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName' import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName'
import useIsFocused from 'wallet/src/features/focus/useIsFocused' import useIsFocused from 'wallet/src/features/focus/useIsFocused'
import { useActiveAccount, useActiveAccountAddress, useDisplayName } from 'wallet/src/features/wallet/hooks' import { useActiveAccount, useActiveAccountAddress, useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
// Value comes from https://uniswapteam.slack.com/archives/C083LU9SD9T/p1733425965373019?thread_ts=1733362029.171999&cid=C083LU9SD9T
const SCAN_ICON_ACTIVE_SCALE = 0.72
const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }): JSX.Element => { const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }): JSX.Element => {
const isScreenFocused = useIsFocused() const isScreenFocused = useIsFocused()
const pressProgress = useSharedValue(0) const pressProgress = useSharedValue(0)
...@@ -61,7 +66,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }): ...@@ -61,7 +66,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }):
return ( return (
<GestureDetector gesture={tap}> <GestureDetector gesture={tap}>
<Animated.View style={animatedStyle}> <Animated.View style={animatedStyle}>
<Settings color="$neutral2" opacity={0.8} size="$icon.24" /> <Settings color="$neutral2" size="$icon.24" />
</Animated.View> </Animated.View>
</GestureDetector> </GestureDetector>
) )
...@@ -112,6 +117,11 @@ export function AccountHeader(): JSX.Element { ...@@ -112,6 +117,11 @@ export function AccountHeader(): JSX.Element {
}) })
} }
} }
const onPressScan = useCallback(async () => {
// in case we received a pending session from a previous scan after closing modal
dispatch(removePendingSession())
dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr }))
}, [dispatch])
const walletHasName = displayName && displayName?.type !== DisplayNameType.Address const walletHasName = displayName && displayName?.type !== DisplayNameType.Address
const iconSize = 52 const iconSize = 52
...@@ -121,50 +131,63 @@ export function AccountHeader(): JSX.Element { ...@@ -121,50 +131,63 @@ export function AccountHeader(): JSX.Element {
{activeAddress && ( {activeAddress && (
<Flex alignItems="flex-start" gap="$spacing12" width="100%"> <Flex alignItems="flex-start" gap="$spacing12" width="100%">
<Flex row justifyContent="space-between" width="100%"> <Flex row justifyContent="space-between" width="100%">
<TouchableArea <Flex shrink row gap="$spacing12">
alignItems="center" <TouchableArea
flexDirection="row" alignItems="center"
hitSlop={20} flexDirection="row"
testID={TestID.AccountHeaderAvatar} hitSlop={20}
onLongPress={async (): Promise<void> => { testID={TestID.AccountHeaderAvatar}
if (isDevEnv()) { onLongPress={async (): Promise<void> => {
dispatch(openModal({ name: ModalName.Experiments })) if (isDevEnv()) {
} dispatch(openModal({ name: ModalName.Experiments }))
}} }
onPress={onPressAccountHeader} }}
> onPress={onPressAccountHeader}
<AccountIcon >
address={activeAddress} <AccountIcon
avatarUri={avatar} address={activeAddress}
showBackground={true} avatarUri={avatar}
showViewOnlyBadge={account?.type === AccountType.Readonly} showBackground={true}
size={iconSize} showViewOnlyBadge={account?.type === AccountType.Readonly}
/> size={iconSize}
</TouchableArea> />
<RotatingSettingsIcon onPressSettings={onPressSettings} />
</Flex>
{walletHasName ? (
<Flex
row
alignItems="center"
gap="$spacing8"
justifyContent="space-between"
testID="account-header/display-name"
>
<TouchableArea flexShrink={1} hitSlop={20} onPress={onPressAccountHeader}>
<AnimatedUnitagDisplayName address={activeAddress} displayName={displayName} />
</TouchableArea> </TouchableArea>
{walletHasName ? (
<Flex
row
shrink
alignSelf="center"
gap="$spacing8"
justifyContent="space-between"
testID="account-header/display-name"
>
<TouchableArea flexGrow={1} hitSlop={20} onPress={onPressAccountHeader}>
<AnimatedUnitagDisplayName address={activeAddress} displayName={displayName} />
</TouchableArea>
</Flex>
) : (
<TouchableArea
alignSelf="center"
hitSlop={20}
testID={TestID.AccountHeaderCopyAddress}
onPress={onPressCopyAddress}
>
<Flex centered row shrink gap="$spacing4">
<Text adjustsFontSizeToFit color="$neutral1" numberOfLines={1} variant="subheading2">
{sanitizeAddressText(shortenAddress(activeAddress))}
</Text>
<CopyAlt color="$neutral2" size="$icon.16" />
</Flex>
</TouchableArea>
)}
</Flex> </Flex>
) : ( <Flex row alignItems="flex-start" gap="$spacing16" pt="$spacing4">
<TouchableArea hitSlop={20} testID={TestID.AccountHeaderCopyAddress} onPress={onPressCopyAddress}> <TouchableArea scaleTo={SCAN_ICON_ACTIVE_SCALE} activeOpacity={1} onPress={onPressScan}>
<Flex centered row shrink gap="$spacing4"> <ScanHome color="$neutral2" size="$icon.24" />
<Text adjustsFontSizeToFit color="$neutral1" numberOfLines={1} variant="subheading2"> </TouchableArea>
{sanitizeAddressText(shortenAddress(activeAddress))} <RotatingSettingsIcon onPressSettings={onPressSettings} />
</Text> </Flex>
<CopyAlt color="$neutral1" size="$icon.16" /> </Flex>
</Flex>
</TouchableArea>
)}
</Flex> </Flex>
)} )}
</Flex> </Flex>
......
import type { Meta, StoryObj } from '@storybook/react' import type { Meta, StoryObj } from '@storybook/react'
import { CopyTextButton } from 'src/components/buttons/CopyTextButton' import { CopyTextButton } from 'src/components/buttons/CopyTextButton'
import { StorybookTitles } from 'ui/src/storybook'
const meta = { const meta = {
title: StorybookTitles.Atoms, title: 'Components/Buttons',
component: CopyTextButton, component: CopyTextButton,
} satisfies Meta<typeof CopyTextButton> } satisfies Meta<typeof CopyTextButton>
......
...@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useState } from 'react' ...@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ListRenderItem, ListRenderItemInfo, StyleSheet } from 'react-native' import { ListRenderItem, ListRenderItemInfo, StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
import { FadeIn, FadeOut, useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' import { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { FavoriteTokensGrid } from 'src/components/explore/FavoriteTokensGrid' import { FavoriteTokensGrid } from 'src/components/explore/FavoriteTokensGrid'
import { FavoriteWalletsGrid } from 'src/components/explore/FavoriteWalletsGrid' import { FavoriteWalletsGrid } from 'src/components/explore/FavoriteWalletsGrid'
...@@ -13,7 +13,7 @@ import { TokenItemData } from 'src/components/explore/TokenItemData' ...@@ -13,7 +13,7 @@ import { TokenItemData } from 'src/components/explore/TokenItemData'
import { AnimatedBottomSheetFlatList } from 'src/components/layout/AnimatedFlatList' import { AnimatedBottomSheetFlatList } from 'src/components/layout/AnimatedFlatList'
import { AutoScrollProps } from 'src/components/sortableGrid/types' import { AutoScrollProps } from 'src/components/sortableGrid/types'
import { getTokenMetadataDisplayType } from 'src/features/explore/utils' import { getTokenMetadataDisplayType } from 'src/features/explore/utils'
import { AnimatedTouchableArea, Flex, Loader, Text, useSporeColors } from 'ui/src' import { Flex, Loader, Text, TouchableArea, useSporeColors } from 'ui/src'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo'
...@@ -191,7 +191,7 @@ function NetworkPillsRow({ ...@@ -191,7 +191,7 @@ function NetworkPillsRow({
const renderItem: ListRenderItem<UniverseChainId> = useCallback( const renderItem: ListRenderItem<UniverseChainId> = useCallback(
({ item }: ListRenderItemInfo<UniverseChainId>) => { ({ item }: ListRenderItemInfo<UniverseChainId>) => {
return ( return (
<AnimatedTouchableArea entering={FadeIn} exiting={FadeOut} onPress={() => onSelectNetwork(item)}> <TouchableArea onPress={() => onSelectNetwork(item)}>
<NetworkPill <NetworkPill
key={item} key={item}
showIcon showIcon
...@@ -207,7 +207,7 @@ function NetworkPillsRow({ ...@@ -207,7 +207,7 @@ function NetworkPillsRow({
showBackgroundColor={false} showBackgroundColor={false}
textVariant="buttonLabel3" textVariant="buttonLabel3"
/> />
</AnimatedTouchableArea> </TouchableArea>
) )
}, },
[colors.neutral1.val, onSelectNetwork, selectedNetwork], [colors.neutral1.val, onSelectNetwork, selectedNetwork],
......
...@@ -176,6 +176,8 @@ export function SearchResultsSection({ ...@@ -176,6 +176,8 @@ export function SearchResultsSection({
return ( return (
<Flex grow gap="$spacing8" pb="$spacing36"> <Flex grow gap="$spacing8" pb="$spacing36">
<AnimatedBottomSheetFlashList <AnimatedBottomSheetFlashList
// when switching networks, we want to rerender the list to prevent any layout misalignments
key={selectedChain}
estimatedItemSize={ESTIMATED_ITEM_SIZE} estimatedItemSize={ESTIMATED_ITEM_SIZE}
ListEmptyComponent={ ListEmptyComponent={
<AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing20"> <AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing20">
......
import React, { useEffect, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import Svg, { Circle } from 'react-native-svg'
import { BackButtonView } from 'src/components/layout/BackButtonView' import { BackButtonView } from 'src/components/layout/BackButtonView'
import { SeedPhraseDisplay } from 'src/components/mnemonic/SeedPhraseDisplay' import { SeedPhraseDisplay } from 'src/components/mnemonic/SeedPhraseDisplay'
import { APP_STORE_LINK } from 'src/constants/urls' import { APP_STORE_LINK } from 'src/constants/urls'
import { UpgradeStatus } from 'src/features/forceUpgrade/types' import { UpgradeStatus } from 'src/features/forceUpgrade/types'
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { Button, Flex, Image, Text, TouchableArea, isWeb, useSporeColors } from 'ui/src'
import { UNISWAP_LOGO } from 'ui/src/assets'
import { imageSizes } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal' import { Modal } from 'uniswap/src/components/modals/Modal'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { NewTag } from 'uniswap/src/components/pill/NewTag'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
import { DynamicConfigs, ForceUpgradeConfigKey } from 'uniswap/src/features/gating/configs' import { DynamicConfigs, ForceUpgradeConfigKey } from 'uniswap/src/features/gating/configs'
import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
...@@ -19,7 +22,7 @@ export function ForceUpgradeModal(): JSX.Element { ...@@ -19,7 +22,7 @@ export function ForceUpgradeModal(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const colors = useSporeColors() const colors = useSporeColors()
const forceUpgradeStatusString = useDynamicConfigValue( const forceUpgradeStatusString = useDynamicConfigValue(
DynamicConfigs.MobileForceUpgrade, DynamicConfigs.ForceUpgrade,
ForceUpgradeConfigKey.Status, ForceUpgradeConfigKey.Status,
'' as string, '' as string,
) )
...@@ -64,26 +67,83 @@ export function ForceUpgradeModal(): JSX.Element { ...@@ -64,26 +67,83 @@ export function ForceUpgradeModal(): JSX.Element {
// the force upgrade screen on error, hence we fallback to the global error boundary // the force upgrade screen on error, hence we fallback to the global error boundary
return ( return (
<> <>
<WarningModal <Modal
acknowledgeText={t('forceUpgrade.action.confirm')} backgroundColor={colors.surface1.val}
hideHandlebar={upgradeStatus === UpgradeStatus.Required} hideHandlebar={upgradeStatus === UpgradeStatus.Required}
isDismissible={upgradeStatus !== UpgradeStatus.Required} isDismissible={upgradeStatus !== UpgradeStatus.Required}
isOpen={isVisible} isModalOpen={isVisible}
modalName={ModalName.ForceUpgradeModal} name={ModalName.ForceUpgradeModal}
severity={WarningSeverity.High}
title={t('forceUpgrade.title')}
onClose={onClose} onClose={onClose}
onAcknowledge={onPressConfirm}
> >
<Text color="$neutral2" textAlign="center" variant="body2"> <Flex
{t('forceUpgrade.description')} centered
</Text> gap="$spacing24"
{mnemonicId && ( pb={isWeb ? '$none' : '$spacing12'}
<Text color="$accent1" variant="buttonLabel2" onPress={onPressViewRecovery}> pt={upgradeStatus === UpgradeStatus.Required ? '$spacing24' : '$spacing12'}
{t('forceUpgrade.action.recoveryPhrase')} px={isWeb ? '$none' : '$spacing24'}
</Text> >
)} <Flex
</WarningModal> centered
width="100%"
height={160}
borderRadius="$rounded16"
borderWidth={1}
borderColor="$surface3"
overflow="hidden"
>
<Flex
centered
borderRadius="$rounded16"
style={{
shadowColor: colors.accent1.val,
elevation: 8,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 20,
}}
borderWidth={1}
borderColor="$surface3"
elevationAndroid={8}
>
<Flex position="relative">
<Image
height={imageSizes.image64}
resizeMode="contain"
source={UNISWAP_LOGO}
width={imageSizes.image64}
/>
<Flex position="absolute" top={-15} right={-8} transform={[{ rotate: '10deg' }]}>
<NewTag exclamation backgroundColor="$accent1" textColor="$white" />
</Flex>
</Flex>
</Flex>
<BackgroundDotPattern />
</Flex>
<Flex gap="$spacing8">
<Text textAlign="center" variant="subheading1">
{t('forceUpgrade.title')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body3">
{t('forceUpgrade.description')}
</Text>
</Flex>
<Flex centered gap="$spacing8" pb={isWeb ? '$none' : '$spacing12'} width="100%">
<Button size="medium" theme="primary" width="100%" onPress={onPressConfirm}>
<Text color="$white" variant="buttonLabel2">
{t('forceUpgrade.action.confirm')}
</Text>
</Button>
{mnemonicId && (
<Button size="medium" theme="secondary" width="100%" onPress={onPressViewRecovery}>
<Text color="$neutral1" variant="buttonLabel2">
{t('forceUpgrade.action.recoveryPhrase')}
</Text>
</Button>
)}
</Flex>
</Flex>
</Modal>
{mnemonicId && showSeedPhrase && ( {mnemonicId && showSeedPhrase && (
<Modal fullScreen backgroundColor={colors.surface1.val} name={ModalName.ForceUpgradeModal} onClose={onDismiss}> <Modal fullScreen backgroundColor={colors.surface1.val} name={ModalName.ForceUpgradeModal} onClose={onDismiss}>
<Flex fill gap="$spacing16" px="$spacing24" py="$spacing24"> <Flex fill gap="$spacing16" px="$spacing24" py="$spacing24">
...@@ -103,3 +163,48 @@ export function ForceUpgradeModal(): JSX.Element { ...@@ -103,3 +163,48 @@ export function ForceUpgradeModal(): JSX.Element {
} }
const BACK_BUTTON_SIZE = 24 const BACK_BUTTON_SIZE = 24
function BackgroundDotPattern(): JSX.Element {
const colors = useSporeColors()
const dotGrid = useMemo(() => {
return Array.from({ length: 100 }).map((_, row) => {
return Array.from({ length: 100 }).map((__, col) => {
const x = col * 2 + 1
const y = row * 2 + 1
const distX = Math.abs(x - 50)
const distY = Math.abs(y - 50)
const dist = Math.sqrt(distX * distX + distY * distY)
if (dist < 45) {
const size = 0.1 + (45 - dist) / 20
return <Circle key={`${row}-${col}`} cx={`${x}%`} cy={`${y}%`} r={size} fill={colors.pinkThemed.val} />
}
return null
})
})
}, [colors])
return (
<Svg width={400} height={400} style={[styles.backgroundPattern, styles.centered]}>
{dotGrid}
</Svg>
)
}
const styles = StyleSheet.create({
backgroundPattern: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
zIndex: -1,
},
centered: {
left: '50%',
top: '50%',
transform: [{ translateX: -200 }, { translateY: -200 }],
},
})
import { expectSaga } from 'redux-saga-test-plan' import { expectSaga } from 'redux-saga-test-plan'
import { call } from 'redux-saga/effects' import { call } from 'redux-saga/effects'
import { navigationRef } from 'src/app/navigation/NavigationContainer' import { navigationRef } from 'src/app/navigation/navigationRef'
import { import {
handleDeepLink, handleDeepLink,
handleUniswapAppDeepLink, handleUniswapAppDeepLink,
......
...@@ -15,6 +15,7 @@ import { ...@@ -15,6 +15,7 @@ import {
UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM, UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM,
UNISWAP_WALLETCONNECT_URL, UNISWAP_WALLETCONNECT_URL,
} from 'src/features/deepLinking/constants' } from 'src/features/deepLinking/constants'
import { handleOffRampReturnLink } from 'src/features/deepLinking/handleOffRampReturnLinkSaga'
import { handleOnRampReturnLink } from 'src/features/deepLinking/handleOnRampReturnLinkSaga' import { handleOnRampReturnLink } from 'src/features/deepLinking/handleOnRampReturnLinkSaga'
import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga' import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga'
import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga' import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga'
...@@ -207,6 +208,7 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) { ...@@ -207,6 +208,7 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) {
const screen = url.searchParams.get('screen') const screen = url.searchParams.get('screen')
const userAddress = url.searchParams.get('userAddress') const userAddress = url.searchParams.get('userAddress')
const fiatOnRamp = url.searchParams.get('fiatOnRamp') === 'true' const fiatOnRamp = url.searchParams.get('fiatOnRamp') === 'true'
const fiatOffRamp = url.searchParams.get('fiatOffRamp') === 'true'
const activeAccount = yield* select(selectActiveAccount) const activeAccount = yield* select(selectActiveAccount)
if (!activeAccount) { if (!activeAccount) {
...@@ -271,6 +273,8 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) { ...@@ -271,6 +273,8 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) {
case 'transaction': case 'transaction':
if (fiatOnRamp) { if (fiatOnRamp) {
yield* call(handleOnRampReturnLink) yield* call(handleOnRampReturnLink)
} else if (fiatOffRamp) {
yield* call(handleOffRampReturnLink, url)
} else { } else {
yield* call(handleTransactionLink) yield* call(handleTransactionLink)
} }
......
import { navigate } from 'src/app/navigation/rootNavigation'
import { openModal } from 'src/features/modals/modalSlice'
import { call, put } from 'typed-redux-saga'
import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FiatOffRampMetaData, OffRampTransferDetailsResponse } from 'uniswap/src/features/fiatOnRamp/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TransactionScreen } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { CurrencyField } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { createTransactionId } from 'uniswap/src/utils/createTransactionId'
import { logger } from 'utilities/src/logger/logger'
import { fetchOffRampTransferDetails } from 'wallet/src/features/fiatOnRamp/api'
import { dismissInAppBrowser } from 'wallet/src/utils/linking'
export function* handleOffRampReturnLink(url: URL) {
try {
yield* call(_handleOffRampReturnLink, url)
} catch (error) {
// TODO: handle error in UI
// Alert.alert(i18n.t('walletConnect.error.general.title'), i18n.t('walletConnect.error.general.message'))
// yield* put(openModal({ name: ModalName.Send, initialState: initialSendState }))
}
}
function* _handleOffRampReturnLink(url: URL) {
const externalTransactionId = url.searchParams.get('externalTransactionId')
if (!externalTransactionId) {
throw new Error('Missing externalTransactionId in fiat offramp deep link')
}
let offRampTransferDetails: OffRampTransferDetailsResponse | undefined
try {
offRampTransferDetails = yield* call(fetchOffRampTransferDetails, externalTransactionId)
} catch (error) {
logger.error(error, {
tags: { file: 'handleOffRampReturnLinkSaga', function: 'handleOffRampReturnLink' },
})
throw new Error('Failed to fetch offramp transfer details')
}
if (!offRampTransferDetails) {
throw new Error('Missing offRampTransferDetails in fiat offramp deep link')
}
const { tokenAddress, baseCurrencyCode, baseCurrencyAmount, depositWalletAddress, logos, provider, chainId } =
offRampTransferDetails
const currencyTradeableAsset: TradeableAsset = {
address: tokenAddress,
chainId: Number(chainId) as UniverseChainId,
type: AssetType.Currency,
}
const fiatOffRampMetaData: FiatOffRampMetaData = {
name: provider,
logoUrl: logos.lightLogo,
// TODO: update activity feed once transaction is submitted
onSubmitCallback: () => {},
moonpayCurrencyCode: baseCurrencyCode,
meldCurrencyCode: baseCurrencyCode,
}
const txnId = createTransactionId()
const initialSendState = {
txId: txnId,
[CurrencyField.INPUT]: currencyTradeableAsset,
[CurrencyField.OUTPUT]: null,
exactCurrencyField: CurrencyField.INPUT,
exactAmountToken: baseCurrencyAmount.toString(),
focusOnCurrencyField: null,
recipient: depositWalletAddress,
isFiatInput: false,
showRecipientSelector: false,
fiatOffRampMetaData,
sendScreen: TransactionScreen.Review,
}
yield* call(navigate, MobileScreens.Home)
yield* put(openModal({ name: ModalName.Send, initialState: initialSendState }))
yield* call(dismissInAppBrowser)
}
...@@ -2,12 +2,9 @@ import { URL } from 'react-native-url-polyfill' ...@@ -2,12 +2,9 @@ import { URL } from 'react-native-url-polyfill'
import { expectSaga } from 'redux-saga-test-plan' import { expectSaga } from 'redux-saga-test-plan'
import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga' import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { DAI, UNI } from 'uniswap/src/constants/tokens' import { DAI, UNI, USDC_UNICHAIN_SEPOLIA } from 'uniswap/src/constants/tokens'
import { AssetType } from 'uniswap/src/entities/assets'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { CurrencyField } from 'uniswap/src/types/currency'
import { signerMnemonicAccount } from 'wallet/src/test/fixtures' import { signerMnemonicAccount } from 'wallet/src/test/fixtures'
const account = signerMnemonicAccount() const account = signerMnemonicAccount()
...@@ -29,44 +26,6 @@ const formSwapUrl = ( ...@@ -29,44 +26,6 @@ const formSwapUrl = (
&amount=${amount}`.trim(), &amount=${amount}`.trim(),
) )
const formTransactionState = (
chain?: UniverseChainId,
inputAddress?: string,
outputAddress?: string,
currencyField?: string,
amount?: string,
): {
input: {
address: string | undefined
chainId: UniverseChainId | undefined
type: AssetType
}
output: {
address: string | undefined
chainId: UniverseChainId | undefined
type: AssetType
}
exactCurrencyField: string | undefined
exactAmountToken: string | undefined
} => ({
[CurrencyField.INPUT]: {
address: inputAddress,
chainId: chain,
type: AssetType.Currency,
},
[CurrencyField.OUTPUT]: {
address: outputAddress,
chainId: chain,
type: AssetType.Currency,
},
exactCurrencyField: !currencyField
? currencyField
: currencyField.toLowerCase() === 'output'
? CurrencyField.OUTPUT
: CurrencyField.INPUT,
exactAmountToken: amount,
})
const swapUrl = formSwapUrl( const swapUrl = formSwapUrl(
account.address, account.address,
UniverseChainId.Mainnet, UniverseChainId.Mainnet,
...@@ -76,6 +35,15 @@ const swapUrl = formSwapUrl( ...@@ -76,6 +35,15 @@ const swapUrl = formSwapUrl(
'100', '100',
) )
const testnetSwapUrl = formSwapUrl(
account.address,
UniverseChainId.Sepolia,
USDC_UNICHAIN_SEPOLIA.address,
UNI[UniverseChainId.Sepolia].address,
'input',
'100',
)
const invalidOutputCurrencySwapUrl = formSwapUrl( const invalidOutputCurrencySwapUrl = formSwapUrl(
account.address, account.address,
UniverseChainId.Mainnet, UniverseChainId.Mainnet,
...@@ -121,19 +89,16 @@ const invalidCurrencyFieldSwapUrl = formSwapUrl( ...@@ -121,19 +89,16 @@ const invalidCurrencyFieldSwapUrl = formSwapUrl(
'100', '100',
) )
const swapFormState = formTransactionState(
UniverseChainId.Mainnet,
DAI.address,
UNI[UniverseChainId.Mainnet].address,
'input',
'100',
) as TransactionState
describe(handleSwapLink, () => { describe(handleSwapLink, () => {
describe('valid inputs', () => { describe('valid inputs', () => {
it('Navigates to the swap screen with all params if all inputs are valid', () => { it('Navigates to the swap screen with all params if all inputs are valid; testnet mode aligned', () => {
return expectSaga(handleSwapLink, swapUrl) return expectSaga(handleSwapLink, swapUrl)
.put(openModal({ name: ModalName.Swap, initialState: swapFormState })) .put(openModal({ name: ModalName.Swap }))
.silentRun()
})
it('Navigates to the swap screen with all params if all inputs are valid; testnet mode not aligned', () => {
return expectSaga(handleSwapLink, testnetSwapUrl)
.put(openModal({ name: ModalName.Swap }))
.silentRun() .silentRun()
}) })
}) })
......
...@@ -2,7 +2,8 @@ import { BigNumber } from 'ethers' ...@@ -2,7 +2,8 @@ import { BigNumber } from 'ethers'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { put } from 'typed-redux-saga' import { put } from 'typed-redux-saga'
import { AssetType, CurrencyAsset } from 'uniswap/src/entities/assets' import { AssetType, CurrencyAsset } from 'uniswap/src/entities/assets'
import { SUPPORTED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types' import { ALL_CHAIN_IDS, SUPPORTED_TESTNET_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types'
import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState' import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { CurrencyField } from 'uniswap/src/types/currency' import { CurrencyField } from 'uniswap/src/types/currency'
...@@ -10,6 +11,16 @@ import { getValidAddress } from 'uniswap/src/utils/addresses' ...@@ -10,6 +11,16 @@ import { getValidAddress } from 'uniswap/src/utils/addresses'
import { currencyIdToAddress, currencyIdToChain } from 'uniswap/src/utils/currencyId' import { currencyIdToAddress, currencyIdToChain } from 'uniswap/src/utils/currencyId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
/**
* Opens swap modal with the provided swap link parameters; prompts testnet switch modal if necessary.
*
* Testing deep links:
* Testnet mode – https://uniswap.org/mobile-redirect?screen=swap&userAddress=<YOUR_WALET_ADDRESS>&inputCurrencyId=41454-0x93EACdB111FF98dE9a8Ac5823d357BBc4842aE63&outputCurrencyId=41454-0xF5A8061bB2C5D9Dc9bC9c5C633D870DAC7bD351e&currencyField=output&amount=100000
* Prod mode – https://uniswap.org/mobile-redirect?screen=swap&userAddress=<YOUR_WALET_ADDRESS>&inputCurrencyId=1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=10-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&currencyField=output&amount=100000
* Mixed – https://uniswap.org/mobile-redirect?screen=swap&userAddress=<YOUR_WALET_ADDRESS>&inputCurrencyId=1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=41454-0xF5A8061bB2C5D9Dc9bC9c5C633D870DAC7bD351e&currencyField=output&amount=100000
*
* @param url - URL object containing the swap link
*/
export function* handleSwapLink(url: URL) { export function* handleSwapLink(url: URL) {
try { try {
const { inputChain, inputAddress, outputChain, outputAddress, exactCurrencyField, exactAmountToken } = const { inputChain, inputAddress, outputChain, outputAddress, exactCurrencyField, exactAmountToken } =
...@@ -34,7 +45,27 @@ export function* handleSwapLink(url: URL) { ...@@ -34,7 +45,27 @@ export function* handleSwapLink(url: URL) {
exactAmountToken, exactAmountToken,
} }
// both should match as of writing because of the check in parseAndValidateSwapParams,
// but we're including an OR gate in case we update to allow only one chain to be passed
const isTestnetChains =
SUPPORTED_TESTNET_CHAIN_IDS.includes(inputChain) || SUPPORTED_TESTNET_CHAIN_IDS.includes(outputChain)
const { isTestnetModeEnabled } = yield* getEnabledChainIdsSaga()
// prefill modal irrespective of testnet mode alignment
yield* put(openModal({ name: ModalName.Swap, initialState: swapFormState })) yield* put(openModal({ name: ModalName.Swap, initialState: swapFormState }))
// if testnet mode isn't aligned with assets, prompt testnet switch modal (closes prefilled swap modal if rejected)
if (isTestnetModeEnabled !== isTestnetChains) {
yield* put(
openModal({
name: ModalName.TestnetSwitchModal,
initialState: {
switchToMode: isTestnetChains ? 'testnet' : 'production',
},
}),
)
return
}
} catch (error) { } catch (error) {
logger.error(error, { tags: { file: 'handleSwapLinkSaga', function: 'handleSwapLink' } }) logger.error(error, { tags: { file: 'handleSwapLinkSaga', function: 'handleSwapLink' } })
yield* put(openModal({ name: ModalName.Swap })) yield* put(openModal({ name: ModalName.Swap }))
...@@ -77,11 +108,11 @@ const parseAndValidateSwapParams = (url: URL) => { ...@@ -77,11 +108,11 @@ const parseAndValidateSwapParams = (url: URL) => {
throw new Error('Invalid tokenAddress provided within outputCurrencyId') throw new Error('Invalid tokenAddress provided within outputCurrencyId')
} }
if (!SUPPORTED_CHAIN_IDS.includes(inputChain)) { if (!ALL_CHAIN_IDS.includes(inputChain)) {
throw new Error('Invalid inputCurrencyId. Chain ID is not supported') throw new Error('Invalid inputCurrencyId. Chain ID is not supported')
} }
if (!SUPPORTED_CHAIN_IDS.includes(outputChain)) { if (!ALL_CHAIN_IDS.includes(outputChain)) {
throw new Error('Invalid outputCurrencyId. Chain ID is not supported') throw new Error('Invalid outputCurrencyId. Chain ID is not supported')
} }
...@@ -95,6 +126,13 @@ const parseAndValidateSwapParams = (url: URL) => { ...@@ -95,6 +126,13 @@ const parseAndValidateSwapParams = (url: URL) => {
throw new Error('Invalid currencyField. Must be either `input` or `output`') throw new Error('Invalid currencyField. Must be either `input` or `output`')
} }
const isInputTestnet = SUPPORTED_TESTNET_CHAIN_IDS.includes(inputChain)
const isOutputTestnet = SUPPORTED_TESTNET_CHAIN_IDS.includes(outputChain)
if (inputChain && outputChain && isInputTestnet !== isOutputTestnet) {
throw new Error('Cannot swap between testnet and mainnet')
}
const exactCurrencyField = currencyField.toLowerCase() === 'output' ? CurrencyField.OUTPUT : CurrencyField.INPUT const exactCurrencyField = currencyField.toLowerCase() === 'output' ? CurrencyField.OUTPUT : CurrencyField.INPUT
return { return {
......
...@@ -2,10 +2,12 @@ import { ExploreModalState } from 'src/app/modals/ExploreModalState' ...@@ -2,10 +2,12 @@ import { ExploreModalState } from 'src/app/modals/ExploreModalState'
import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState' import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchModalState'
import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState' import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState'
import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types' import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TransactionScreen } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState' import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
...@@ -33,8 +35,9 @@ export interface ModalsState { ...@@ -33,8 +35,9 @@ export interface ModalsState {
[ModalName.RemoveWallet]: AppModalState<RemoveWalletModalState> [ModalName.RemoveWallet]: AppModalState<RemoveWalletModalState>
[ModalName.RestoreWallet]: AppModalState<undefined> [ModalName.RestoreWallet]: AppModalState<undefined>
[ModalName.Scantastic]: AppModalState<ScantasticModalState> [ModalName.Scantastic]: AppModalState<ScantasticModalState>
[ModalName.Send]: AppModalState<TransactionState> [ModalName.Send]: AppModalState<TransactionState & { sendScreen: TransactionScreen }>
[ModalName.Swap]: AppModalState<TransactionState> [ModalName.Swap]: AppModalState<TransactionState>
[ModalName.TestnetSwitchModal]: AppModalState<TestnetSwitchModalState>
[ModalName.UnitagsIntro]: AppModalState<{ [ModalName.UnitagsIntro]: AppModalState<{
address: Address address: Address
entryPoint: MobileScreens.Home | MobileScreens.Settings entryPoint: MobileScreens.Home | MobileScreens.Settings
......
...@@ -5,9 +5,11 @@ import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWallet ...@@ -5,9 +5,11 @@ import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWallet
import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState' import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState'
import { ModalsState } from 'src/features/modals/ModalsState' import { ModalsState } from 'src/features/modals/ModalsState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchModalState'
import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState' import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState'
import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TransactionScreen } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState' import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { getKeys } from 'utilities/src/primitives/objects' import { getKeys } from 'utilities/src/primitives/objects'
...@@ -60,6 +62,11 @@ type ScantasticModalParams = { ...@@ -60,6 +62,11 @@ type ScantasticModalParams = {
initialState: ScantasticModalState initialState: ScantasticModalState
} }
type TestnetSwitchModalParams = {
name: typeof ModalName.TestnetSwitchModal
initialState?: TestnetSwitchModalState
}
type RemoveWalletModalParams = { type RemoveWalletModalParams = {
name: typeof ModalName.RemoveWallet name: typeof ModalName.RemoveWallet
initialState?: RemoveWalletModalState initialState?: RemoveWalletModalState
...@@ -74,7 +81,12 @@ type WalletConnectModalParams = { ...@@ -74,7 +81,12 @@ type WalletConnectModalParams = {
type SwapModalParams = { name: typeof ModalName.Swap; initialState?: TransactionState } type SwapModalParams = { name: typeof ModalName.Swap; initialState?: TransactionState }
type SendModalParams = { name: typeof ModalName.Send; initialState?: TransactionState } type SendModalParams = {
name: typeof ModalName.Send
initialState?: TransactionState & {
sendScreen?: TransactionScreen
}
}
type UnitagsIntroParams = { type UnitagsIntroParams = {
name: typeof ModalName.UnitagsIntro name: typeof ModalName.UnitagsIntro
...@@ -114,6 +126,7 @@ export type OpenModalParams = ...@@ -114,6 +126,7 @@ export type OpenModalParams =
| ReceiveCryptoModalParams | ReceiveCryptoModalParams
| LanguageSelectorModalParams | LanguageSelectorModalParams
| ScantasticModalParams | ScantasticModalParams
| TestnetSwitchModalParams
| RemoveWalletModalParams | RemoveWalletModalParams
| SendModalParams | SendModalParams
| SwapModalParams | SwapModalParams
......
import { useDispatch } from 'react-redux'
import { promptPushPermission } from 'src/features/notifications/Onesignal'
import {
NotificationPermission,
useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { useNotificationToggle } from 'src/features/notifications/hooks/useNotificationsToggle'
import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen'
import { act, renderHook, waitFor } from 'src/test/test-utils'
import { useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks'
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'), // Keep all other exports
useDispatch: jest.fn(),
}))
jest.mock('src/features/notifications/Onesignal', () => ({
promptPushPermission: jest.fn(),
}))
jest.mock('src/features/notifications/hooks/useNotificationOSPermissionsEnabled', () => ({
...jest.requireActual('src/features/notifications/hooks/useNotificationOSPermissionsEnabled'),
useNotificationOSPermissionsEnabled: jest.fn(),
}))
jest.mock('wallet/src/features/wallet/accounts/editAccountSaga', () => ({
...jest.requireActual('wallet/src/features/wallet/accounts/editAccountSaga'),
editAccountActions: {
trigger: jest.fn((payload) => ({ type: 'EDIT_ACCOUNT', payload })),
},
}))
jest.mock('src/screens/Onboarding/NotificationsSetupScreen', () => ({
showNotificationSettingsAlert: jest.fn(),
}))
jest.mock('src/utils/useAppStateTrigger', () => ({
useAppStateTrigger: jest.fn(),
}))
jest.mock('wallet/src/features/wallet/hooks', () => ({
useSelectAccountNotificationSetting: jest.fn(),
}))
describe('useNotificationToggle', () => {
const mockAddress = '0xAddress'
const mockDispatch = jest.fn()
const mockUseDispatch = useDispatch as jest.MockedFunction<typeof useDispatch>
const mockPermissionPrompt = jest.mocked(promptPushPermission)
const mockSettingsAlert = jest.mocked(showNotificationSettingsAlert)
const mockUseSelectAccountNotificationSetting = jest.mocked(useSelectAccountNotificationSetting)
const mockUseNotificationOSPermissionsQuery = jest.mocked(useNotificationOSPermissionsEnabled)
beforeEach(() => {
jest.clearAllMocks()
mockUseDispatch.mockReturnValue(mockDispatch)
})
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function setupHook({
osPermissionStatus = NotificationPermission.Enabled,
firebaseEnabled = true,
onPermissionChanged,
}: {
osPermissionStatus?: NotificationPermission
firebaseEnabled?: boolean
onPermissionChanged?: (enabled: boolean) => void
} = {}) {
mockUseNotificationOSPermissionsQuery.mockReturnValue(osPermissionStatus)
mockUseSelectAccountNotificationSetting.mockReturnValue(firebaseEnabled)
return renderHook(() => useNotificationToggle({ address: mockAddress, onPermissionChanged }))
}
describe('initial states', () => {
it('returns enabled when both OS and Firebase are enabled', () => {
const { result } = setupHook({
osPermissionStatus: NotificationPermission.Enabled,
firebaseEnabled: true,
})
expect(result.current.isEnabled).toBe(true)
expect(result.current.isPending).toBe(false)
})
it('returns disabled when OS permissions are disabled', () => {
const { result } = setupHook({
osPermissionStatus: NotificationPermission.Disabled,
firebaseEnabled: true,
})
expect(result.current.isEnabled).toBe(false)
expect(result.current.isPending).toBe(false)
})
it('returns disabled when Firebase is disabled', () => {
const { result } = setupHook({
osPermissionStatus: NotificationPermission.Enabled,
firebaseEnabled: false,
})
expect(result.current.isEnabled).toBe(false)
expect(result.current.isPending).toBe(false)
})
})
describe('toggle flows', () => {
// For the toggle test
it('handles toggle when OS permissions are enabled', async () => {
const { result } = setupHook({
osPermissionStatus: NotificationPermission.Enabled,
firebaseEnabled: true,
})
await act(async () => {
result.current.toggle()
await new Promise(requestAnimationFrame)
})
await waitFor(() => {
expect(mockDispatch).toHaveBeenCalled()
expect(result.current.isEnabled).toBe(false)
})
})
it('handles OS permission prompt flow successfully', async () => {
mockPermissionPrompt.mockResolvedValueOnce(true)
const { result } = setupHook({
osPermissionStatus: NotificationPermission.Disabled,
firebaseEnabled: false,
})
await act(async () => {
result.current.toggle()
await new Promise(requestAnimationFrame)
})
await waitFor(() => {
expect(mockPermissionPrompt).toHaveBeenCalled()
expect(mockDispatch).toHaveBeenCalled()
expect(result.current.isEnabled).toBe(true)
})
})
it('handles OS permission prompt flow failure', async () => {
mockPermissionPrompt.mockResolvedValueOnce(false)
const { result } = setupHook({
osPermissionStatus: NotificationPermission.Disabled,
firebaseEnabled: false,
})
await act(async () => {
result.current.toggle()
await new Promise(requestAnimationFrame)
})
await waitFor(() => {
expect(mockPermissionPrompt).toHaveBeenCalled()
expect(mockSettingsAlert).toHaveBeenCalled()
expect(result.current.isEnabled).toBe(false)
expect(result.current.isPending).toBe(false)
})
})
})
describe('callbacks', () => {
it('calls onPermissionChanged after successful toggle', async () => {
const onPermissionChanged = jest.fn()
const { result } = setupHook({
osPermissionStatus: NotificationPermission.Enabled,
firebaseEnabled: false,
onPermissionChanged,
})
await act(async () => {
result.current.toggle()
await new Promise(requestAnimationFrame)
})
await waitFor(() => {
expect(onPermissionChanged).toHaveBeenCalledWith(true)
})
})
})
describe('error handling', () => {
it('shows settings alert and resets state on OS permission denial', async () => {
mockPermissionPrompt.mockResolvedValueOnce(false)
const { result } = setupHook({
osPermissionStatus: NotificationPermission.Disabled,
})
await act(async () => {
result.current.toggle()
await new Promise(requestAnimationFrame)
})
await waitFor(() => {
expect(mockSettingsAlert).toHaveBeenCalled()
expect(result.current.isEnabled).toBe(false)
})
})
})
})
import { useMutation } from '@tanstack/react-query'
import { useCallback, useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { promptPushPermission } from 'src/features/notifications/Onesignal'
import {
NotificationPermission,
useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks'
// Wait for next frame to ensure UI updates without flashing
// https://corbt.com/posts/2015/12/22/breaking-up-heavy-processing-in-react-native.html
const waitFrame = async (): Promise<void> => {
await new Promise(requestAnimationFrame)
}
enum NotificationError {
OsPermissionDenied = 'OS_PERMISSION_DENIED',
}
/**
* useNotificationToggle
*
* Enabling notifications across different initial states.
*
* What goes into enabling notifications?
* - OS permissions
* - Firebase settings (via redux)
*
* What states can the user be in?
* - Denied notifications at the OS level
* - Enabled notifications at the OS level
* - Denied notifications during onboarding
* - Enabled notifications during onboarding
* - Skipped notifications during onboarding
*
* Situation A: Denied notifications at the OS level
* - User goes to toggle notifications
* - We optimistically enable firebase settings
* - We prompt the user to go to settings and re-enable notifications
* - App is backgrounded and user goes to settings
* - App is foregrounded and we refetch the OS permissions
* - Notifications are enabled
*
* Situation B: User skipped notifications during onboarding
* - User goes to toggle notifications
* - We request the OS permissions
* - Notifications are enabled
*
* Situation C: User has notifications enabled but wants to disable
* - User goes to toggle notifications
* - We disable Firebase settings immediately
* - OS permissions remain enabled but notifications stop
* - User can re-enable without OS prompt
*
* Situation D: User enabled during onboarding but removed OS permissions later
* - User has Firebase enabled but OS permissions are off
* - User goes to toggle notifications
* - We detect mismatched state
* - We maintain Firebase settings as enabled
* - We prompt user to restore OS permissions
* - Normal OS permission flow resumes
*/
export function useNotificationToggle(props: { address: string; onPermissionChanged?: (enabled: boolean) => void }): {
isEnabled: boolean
isPending: boolean
toggle: () => void
} {
const dispatch = useDispatch()
// Get real states from different systems
const osPermissionStatus = useNotificationOSPermissionsEnabled()
const reduxPushNotificationsEnabled = useSelectAccountNotificationSetting(props.address)
const isOSPermissionEnabled = osPermissionStatus === NotificationPermission.Enabled
// Derive real enabled state - only true if both systems are enabled
const isEnabled = isOSPermissionEnabled && reduxPushNotificationsEnabled
// Optimistic UI state
const [optimisticEnabled, setOptimisticEnabled] = useState<boolean>(isEnabled)
// Helper to handle OS permission request and state update
const requestOSPermissions = useCallback(async (): Promise<true> => {
const granted = await promptPushPermission()
if (!granted) {
// first let's enable the redux state (firebase)
// this will ensure that when the user goes to settings and enables notifications
// we're not stuck in a state where notifications are disabled
// and the user has to hit the toggle again
dispatch(
editAccountActions.trigger({
type: EditAccountAction.TogglePushNotification,
enabled: true,
address: props.address,
}),
)
// this means the user denied the permission at the system level
// and needs to go to settings to re-enable (boo)
throw new Error(NotificationError.OsPermissionDenied)
}
return true
}, [dispatch, props.address])
// Reset optimistic state if real state changes
useEffect(() => {
setOptimisticEnabled(isEnabled)
}, [isEnabled])
const mutation = useMutation({
onMutate: async () => {
// Wait for next frame to ensure UI updates without flashing
await waitFrame()
// Only show optimistic updates when we have OS permissions
if (isOSPermissionEnabled) {
setOptimisticEnabled(!optimisticEnabled)
}
},
mutationFn: async () => {
const isOsEnabled = isOSPermissionEnabled || (await requestOSPermissions())
// After this point, we're guaranteed to have requested OS permissions
// If we just obtained permissions, we want to enable notifications
// Otherwise, we're toggling the current redux state
const shouldEnable = isOsEnabled ? !reduxPushNotificationsEnabled : true
dispatch(
editAccountActions.trigger({
type: EditAccountAction.TogglePushNotification,
enabled: shouldEnable,
address: props.address,
}),
)
return shouldEnable
},
onError: (error) => {
if (error.message === NotificationError.OsPermissionDenied) {
// user will need to go to settings to re-enable notifications
// when they come back, the real state will be refetched and the UI will update automatically
showNotificationSettingsAlert()
setOptimisticEnabled(false)
}
},
onSuccess: (enabled) => {
// setState will bail if the value is the same as the current state
// so we can safely call it without conditionals
setOptimisticEnabled(enabled)
props.onPermissionChanged?.(enabled)
},
})
return {
isEnabled: optimisticEnabled,
isPending: mutation.isPending,
toggle: () => mutation.mutate(),
}
}
...@@ -41,6 +41,8 @@ export function OnboardingScreen({ ...@@ -41,6 +41,8 @@ export function OnboardingScreen({
const headerHeight = useHeaderHeight() const headerHeight = useHeaderHeight()
const insets = useAppInsets() const insets = useAppInsets()
const media = useMedia() const media = useMedia()
// TODO(WALL-5483): remove this once we improve seed recovery screen design on smaller devices
const showIcon = !media.short
const gapSize = media.short ? '$none' : '$spacing16' const gapSize = media.short ? '$none' : '$spacing16'
...@@ -82,7 +84,7 @@ export function OnboardingScreen({ ...@@ -82,7 +84,7 @@ export function OnboardingScreen({
> >
{/* Text content */} {/* Text content */}
<Flex centered gap="$spacing8" m="$spacing12" mb={ignoreTextContainerMarginBottom ? '$none' : undefined}> <Flex centered gap="$spacing8" m="$spacing12" mb={ignoreTextContainerMarginBottom ? '$none' : undefined}>
{Icon && ( {showIcon && Icon && (
<Flex centered mb="$spacing4"> <Flex centered mb="$spacing4">
<Flex centered backgroundColor="$surface3" borderRadius="$rounded8" p="$spacing12"> <Flex centered backgroundColor="$surface3" borderRadius="$rounded8" p="$spacing12">
<Icon color="$neutral1" size="$icon.18" /> <Icon color="$neutral1" size="$icon.18" />
......
...@@ -44,16 +44,20 @@ export function SendFlow(): JSX.Element { ...@@ -44,16 +44,20 @@ export function SendFlow(): JSX.Element {
onClose={onClose} onClose={onClose}
> >
<SendContextProvider prefilledTransactionState={initialState}> <SendContextProvider prefilledTransactionState={initialState}>
<CurrentScreen /> <CurrentScreen screenOverride={initialState?.sendScreen} />
</SendContextProvider> </SendContextProvider>
</TransactionModal> </TransactionModal>
) )
} }
function CurrentScreen(): JSX.Element { function CurrentScreen({ screenOverride }: { screenOverride?: TransactionScreen }): JSX.Element {
const { screen } = useTransactionModalContext() const { screen, setScreen } = useTransactionModalContext()
const { recipient } = useSendContext() const { recipient } = useSendContext()
if (screenOverride) {
setScreen(screenOverride)
}
// If no recipient, force full screen recipient select. Need to render this outside of `SendFormScreen` to ensure that // If no recipient, force full screen recipient select. Need to render this outside of `SendFormScreen` to ensure that
// the modals are rendered correctly, and animations can properly measure the available space for the decimal pad. // the modals are rendered correctly, and animations can properly measure the available space for the decimal pad.
if (!recipient) { if (!recipient) {
......
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState'
import { Wrench } from 'ui/src/components/icons'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
import { setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice'
import { ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
export function TestnetSwitchModal(): JSX.Element {
const dispatch = useDispatch()
const { t } = useTranslation()
const modalState = useSelector(selectModalState(ModalName.TestnetSwitchModal))
const { switchToMode } = modalState.initialState ?? {}
const onToggleTestnetMode = (): void => {
dispatch(closeModal({ name: ModalName.TestnetSwitchModal }))
dispatch(setIsTestnetModeEnabled(switchToMode === 'testnet'))
sendAnalyticsEvent(WalletEventName.TestnetModeToggled, {
enabled: switchToMode === 'testnet',
location: 'deep_link_modal',
})
}
const onReject = (): void => {
dispatch(closeModal({ name: ModalName.Swap }))
dispatch(closeModal({ name: ModalName.TestnetSwitchModal }))
}
const toTestnetModeDescription = t('testnet.modal.swapDeepLink.description.toTestnetMode')
const toProdModeDescription = t('testnet.modal.swapDeepLink.description.toProdMode')
const toTestnetModeTitle = t('testnet.modal.swapDeepLink.title.toTestnetMode')
const toProdModeTitle = t('testnet.modal.swapDeepLink.title.toProdMode')
return (
<WarningModal
isOpen
caption={switchToMode === 'production' ? toProdModeDescription : toTestnetModeDescription}
rejectText={t('common.button.cancel')}
acknowledgeText={t('common.button.confirm')}
icon={<Wrench color="$neutral1" size="$icon.24" />}
acknowledgeButtonTheme="primary"
// only show if swap form state is provided
modalName={ModalName.TestnetSwitchModal}
severity={WarningSeverity.None}
title={switchToMode === 'production' ? toProdModeTitle : toTestnetModeTitle}
onAcknowledge={onToggleTestnetMode}
onClose={onReject}
onReject={onReject}
/>
)
}
export interface TestnetSwitchModalState {
switchToMode: 'testnet' | 'production'
}
...@@ -159,26 +159,19 @@ export const parseTransactionRequest = ( ...@@ -159,26 +159,19 @@ export const parseTransactionRequest = (
} }
} }
function isUtf8(str: string): boolean { export function decodeMessage(value: string): string {
try { try {
const decoded = new TextDecoder('utf-8').decode(new TextEncoder().encode(str)) if (utils.isHexString(value)) {
const decoded = utils.toUtf8String(value)
if (decoded?.trim()) {
return decoded
}
}
// if the encoded -> decoded string matches the original string (ie no chars swapped), return value
// then it's valid utf-8
return decoded === str
} catch { } catch {
return false return value
}
}
export function decodeMessage(value: string): string {
if (utils.isHexString(value) && isUtf8(value)) {
const decoded = utils.toUtf8String(value)
if (decoded?.trim()) {
return decoded
}
} }
return value
} }
/** /**
......
...@@ -53,6 +53,8 @@ export function ExploreScreen(): JSX.Element { ...@@ -53,6 +53,8 @@ export function ExploreScreen(): JSX.Element {
const [isSearchMode, setIsSearchMode] = useState<boolean>(false) const [isSearchMode, setIsSearchMode] = useState<boolean>(false)
const textInputRef = useRef<TextInput>(null) const textInputRef = useRef<TextInput>(null)
const [selectedChain, setSelectedChain] = useState<UniverseChainId | null>(null) const [selectedChain, setSelectedChain] = useState<UniverseChainId | null>(null)
// TODO(WALL-5482): investigate list rendering performance/scrolling issue
const canRenderList = useRenderNextFrame(!isSearchMode)
const onSearchChangeText = (newSearchFilter: string): void => { const onSearchChangeText = (newSearchFilter: string): void => {
setSearchQuery(newSearchFilter) setSearchQuery(newSearchFilter)
...@@ -132,8 +134,56 @@ export function ExploreScreen(): JSX.Element { ...@@ -132,8 +134,56 @@ export function ExploreScreen(): JSX.Element {
)} )}
</KeyboardAvoidingView> </KeyboardAvoidingView>
) : ( ) : (
isSheetReady && <ExploreSections listRef={listRef} /> isSheetReady && canRenderList && <ExploreSections listRef={listRef} />
)} )}
</Screen> </Screen>
) )
} }
/**
* A hook that safely handles mounting/unmounting using requestAnimationFrame.
* This can help prevent common React Native issues with rendering and gestures
* by ensuring elements mount on the next frame. (not ideal, but better than nothing)
*/
const useRenderNextFrame = (condition: boolean): boolean => {
const [canRender, setCanRender] = useState<boolean>(false)
const rafRef = useRef<number>()
const mountedRef = useRef<boolean>(true)
const conditionRef = useRef<boolean>(condition)
// clean up on unmount to prevent memory leaks
useEffect(() => {
return () => {
mountedRef.current = false
if (rafRef.current) {
cancelAnimationFrame(rafRef.current)
}
}
}, [])
// schedule render for next frame if we should mount
useEffect(() => {
conditionRef.current = condition
if (condition) {
rafRef.current = requestAnimationFrame(() => {
// By the time this callback runs, 'condition' might have changed
// since RAF executes in the next frame, so we store the condition in a ref
if (mountedRef.current && conditionRef.current) {
setCanRender(true)
}
})
} else {
setCanRender(false)
}
return () => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current)
}
}
}, [condition])
return canRender
}
...@@ -106,7 +106,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | ...@@ -106,7 +106,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
refundWalletAddress: activeAccountAddress, refundWalletAddress: activeAccountAddress,
externalCustomerId: activeAccountAddress, externalCustomerId: activeAccountAddress,
externalSessionId: externalTransactionId, externalSessionId: externalTransactionId,
redirectUrl: `${uniswapUrls.redirectUrlBase}?screen=transaction&fiatOffRamp=true&userAddress=${activeAccountAddress}`, redirectUrl: `${uniswapUrls.redirectUrlBase}?screen=transaction&fiatOffRamp=true&userAddress=${activeAccountAddress}&externalTransactionId=${externalTransactionId}`,
} }
: skipToken, : skipToken,
) )
......
...@@ -366,6 +366,14 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -366,6 +366,14 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
}) })
const onSelectCurrency = (currency: FORCurrencyOrBalance): void => { const onSelectCurrency = (currency: FORCurrencyOrBalance): void => {
if (isTokenInputMode) {
resetAmount()
} else {
setSelectedQuote(undefined)
// This is done for formatting reasons. The existing value may change if max decimals of new currency is different
onChangeValue(value, 'changeAsset')
}
setShowTokenSelector(false) setShowTokenSelector(false)
if (isSupportedFORCurrency(currency)) { if (isSupportedFORCurrency(currency)) {
setQuoteCurrency(currency) setQuoteCurrency(currency)
...@@ -427,14 +435,18 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -427,14 +435,18 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
} }
}, [navigateToSwapFlow, unsupportedCurrency]) }, [navigateToSwapFlow, unsupportedCurrency])
const onPillToggle = (option: string | number): void => { const resetAmount = useCallback(() => {
setIsOffRamp(option === RampToggle.SELL)
setValue('') setValue('')
setFiatAmount(0) setFiatAmount(0)
setTokenAmount(0) setTokenAmount(0)
valueRef.current = '' valueRef.current = ''
resetSelection({ start: 0 }) resetSelection({ start: 0 })
setSelectedQuote(undefined)
}, [setValue, setFiatAmount, setTokenAmount, valueRef, resetSelection, setSelectedQuote])
const onPillToggle = (option: string | number): void => {
setIsOffRamp(option === RampToggle.SELL)
resetAmount()
setQuoteCurrency(defaultCurrency) setQuoteCurrency(defaultCurrency)
sendAnalyticsEvent(FiatOffRampEventName.FORBuySellToggled, { sendAnalyticsEvent(FiatOffRampEventName.FORBuySellToggled, {
......
...@@ -15,7 +15,6 @@ import Animated, { ...@@ -15,7 +15,6 @@ import Animated, {
useDerivedValue, useDerivedValue,
useSharedValue, useSharedValue,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { SvgProps } from 'react-native-svg'
import { SceneRendererProps, TabBar } from 'react-native-tab-view' import { SceneRendererProps, TabBar } from 'react-native-tab-view'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { NavBar, SWAP_BUTTON_HEIGHT } from 'src/app/navigation/NavBar' import { NavBar, SWAP_BUTTON_HEIGHT } from 'src/app/navigation/NavBar'
...@@ -45,16 +44,12 @@ import { openModal } from 'src/features/modals/modalSlice' ...@@ -45,16 +44,12 @@ import { openModal } from 'src/features/modals/modalSlice'
import { selectSomeModalOpen } from 'src/features/modals/selectSomeModalOpen' import { selectSomeModalOpen } from 'src/features/modals/selectSomeModalOpen'
import { AIAssistantOverlay } from 'src/features/openai/AIAssistantOverlay' import { AIAssistantOverlay } from 'src/features/openai/AIAssistantOverlay'
import { useWalletRestore } from 'src/features/wallet/hooks' import { useWalletRestore } from 'src/features/wallet/hooks'
import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback' import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { hideSplashScreen } from 'src/utils/splashScreen' import { hideSplashScreen } from 'src/utils/splashScreen'
import { useOpenBackupReminderModal } from 'src/utils/useOpenBackupReminderModal' import { useOpenBackupReminderModal } from 'src/utils/useOpenBackupReminderModal'
import { Flex, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src' import { Flex, GeneratedIcon, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src'
import ReceiveIcon from 'ui/src/assets/icons/arrow-down-circle.svg' import { ArrowDownCircle, Buy, SendAction } from 'ui/src/components/icons'
import BuyIcon from 'ui/src/assets/icons/buy.svg'
import ScanIcon from 'ui/src/assets/icons/scan-home.svg'
import SendIcon from 'ui/src/assets/icons/send-action.svg'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
...@@ -376,12 +371,6 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX. ...@@ -376,12 +371,6 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
await hapticFeedback.light() await hapticFeedback.light()
}, [hapticFeedback]) }, [hapticFeedback])
const onPressScan = useCallback(async () => {
// in case we received a pending session from a previous scan after closing modal
dispatch(removePendingSession())
dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr }))
await triggerHaptics()
}, [dispatch, triggerHaptics])
const onPressSend = useCallback(async () => { const onPressSend = useCallback(async () => {
dispatch(openModal({ name: ModalName.Send })) dispatch(openModal({ name: ModalName.Send }))
await triggerHaptics() await triggerHaptics()
...@@ -400,11 +389,6 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX. ...@@ -400,11 +389,6 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
// Hide actions when active account isn't a signer account. // Hide actions when active account isn't a signer account.
const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic
// Necessary to declare these as direct dependencies due to race condition with initializing react-i18next and useMemo
const buyLabel = t('home.label.buy')
const sendLabel = t('home.label.send')
const receiveLabel = t('home.label.receive')
const scanLabel = t('home.label.scan')
const [isTestnetWarningModalOpen, setIsTestnetWarningModalOpen] = useState(false) const [isTestnetWarningModalOpen, setIsTestnetWarningModalOpen] = useState(false)
...@@ -425,41 +409,34 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX. ...@@ -425,41 +409,34 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
) )
}, [dispatch, isTestnetModeEnabled, disableForKorea, triggerHaptics]) }, [dispatch, isTestnetModeEnabled, disableForKorea, triggerHaptics])
// Necessary to declare these as direct dependencies due to race condition with initializing react-i18next and useMemo
const buyLabel = t('home.label.buy')
const sendLabel = t('home.label.send')
const receiveLabel = t('home.label.receive')
const actions = useMemo( const actions = useMemo(
(): QuickAction[] => [ (): QuickAction[] => [
{ {
Icon: BuyIcon, Icon: Buy,
eventName: MobileEventName.FiatOnRampQuickActionButtonPressed, eventName: MobileEventName.FiatOnRampQuickActionButtonPressed,
iconScale: 1.2,
label: buyLabel, label: buyLabel,
name: ElementName.Buy, name: ElementName.Buy,
sentryLabel: 'BuyActionButton',
onPress: onPressBuy, onPress: onPressBuy,
}, },
{ {
Icon: SendIcon, Icon: SendAction,
iconScale: 1.1,
label: sendLabel, label: sendLabel,
name: ElementName.Send, name: ElementName.Send,
sentryLabel: 'SendActionButton',
onPress: onPressSend, onPress: onPressSend,
}, },
{ {
Icon: ReceiveIcon, Icon: ArrowDownCircle,
label: receiveLabel, label: receiveLabel,
name: ElementName.Receive, name: ElementName.Receive,
sentryLabel: 'ReceiveActionButton',
onPress: onPressReceive, onPress: onPressReceive,
}, },
{
Icon: ScanIcon,
label: scanLabel,
name: ElementName.WalletConnectScan,
sentryLabel: 'ScanActionButton',
onPress: onPressScan,
},
], ],
[buyLabel, sendLabel, scanLabel, receiveLabel, onPressBuy, onPressScan, onPressSend, onPressReceive], [buyLabel, sendLabel, receiveLabel, onPressBuy, onPressSend, onPressReceive],
) )
// This hooks handles the logic for when to open the BackupReminderModal // This hooks handles the logic for when to open the BackupReminderModal
...@@ -479,12 +456,7 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX. ...@@ -479,12 +456,7 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
const contentHeader = useMemo(() => { const contentHeader = useMemo(() => {
return ( return (
<Flex <Flex backgroundColor="$surface1" pb={showEmptyWalletState ? '$spacing8' : '$spacing16'} px="$spacing12">
backgroundColor="$surface1"
gap="$spacing8"
pb={showEmptyWalletState ? '$spacing8' : '$spacing16'}
px="$spacing12"
>
<TestnetModeModal <TestnetModeModal
unsupported unsupported
isOpen={isTestnetWarningModalOpen} isOpen={isTestnetWarningModalOpen}
...@@ -492,15 +464,15 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX. ...@@ -492,15 +464,15 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
onClose={handleTestnetWarningModalClose} onClose={handleTestnetWarningModalClose}
/> />
<AccountHeader /> <AccountHeader />
<Flex pb="$spacing8" px="$spacing12"> <Flex py="$spacing20" px="$spacing12">
<PortfolioBalance owner={activeAccount.address} /> <PortfolioBalance owner={activeAccount.address} />
</Flex> </Flex>
{isSignerAccount ? ( {isSignerAccount ? (
<QuickActions actions={actions} sentry-label="QuickActions" /> <QuickActions actions={actions} />
) : ( ) : (
<TouchableArea mt="$spacing16" onPress={onPressViewOnlyLabel}> <TouchableArea mt="$spacing8" onPress={onPressViewOnlyLabel}>
<Flex centered row backgroundColor="$surface2" borderRadius="$rounded12" minHeight={40} p="$spacing8"> <Flex centered row backgroundColor="$surface2" borderRadius="$rounded12" minHeight={40} p="$spacing8">
<Text allowFontScaling={false} color="$neutral2" variant="body2"> <Text color="$neutral2" variant="body2">
{viewOnlyLabel} {viewOnlyLabel}
</Text> </Text>
</Flex> </Flex>
...@@ -797,69 +769,51 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX. ...@@ -797,69 +769,51 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
} }
type QuickAction = { type QuickAction = {
Icon: React.FC<SvgProps> /* Icon to display for the action */
Icon: GeneratedIcon
/* Event name to log when the action is triggered */
eventName?: MobileEventName eventName?: MobileEventName
iconScale?: number /* Label to display for the action */
label: string label: string
/* Name of the element to log when the action is triggered */
name: ElementNameType name: ElementNameType
sentryLabel: string /* Callback to execute when the action is triggered */
onPress: () => void onPress: () => void
} }
/**
* CTA buttons that appear at top of the screen showing actions such as
* "Send", "Receive", "Buy" etc.
*/
function QuickActions({ actions }: { actions: QuickAction[] }): JSX.Element { function QuickActions({ actions }: { actions: QuickAction[] }): JSX.Element {
const colors = useSporeColors()
const iconSize = iconSizes.icon24
const contentColor = colors.accent1.val
const activeScale = 0.96
return ( return (
<Flex centered row gap="$spacing12" px="$spacing12"> <Flex centered row gap="$spacing12" px="$spacing12">
{actions.map((action) => ( {actions.map(({ eventName, name, label, Icon, onPress }) => (
<ActionButton <Trace key={name} logPress element={name} eventOnTrigger={eventName}>
key={action.name} <TouchableArea flex={1} scaleTo={activeScale} onPress={onPress}>
Icon={action.Icon} <Flex
eventName={action.eventName} fill
flex={1} backgroundColor="$accent2"
iconScale={action.iconScale} borderRadius="$rounded20"
label={action.label} pt="$spacing16"
name={action.name} pb="$spacing12"
sentry-label={action.sentryLabel} px="$spacing12"
onPress={action.onPress} gap="$spacing4"
/> justifyContent="space-between"
>
<Icon color={contentColor} size={iconSize} strokeWidth={2} />
<Text color={contentColor} variant="buttonLabel2">
{label}
</Text>
</Flex>
</TouchableArea>
</Trace>
))} ))}
</Flex> </Flex>
) )
} }
function ActionButton({
eventName,
name,
Icon,
onPress,
flex,
activeScale = 0.96,
iconScale = 1,
}: {
eventName?: MobileEventName
name: ElementNameType
label: string
Icon: React.FC<SvgProps>
onPress: () => void
flex: number
activeScale?: number
iconScale?: number
}): JSX.Element {
const colors = useSporeColors()
const media = useMedia()
const iconSize = media.short ? iconSizes.icon24 : iconSizes.icon28
return (
<Trace logPress element={name} eventOnTrigger={eventName}>
<TouchableArea flex={flex} scaleTo={activeScale} onPress={onPress}>
<AnimatedFlex centered fill backgroundColor="$accent2" borderRadius="$rounded20" p="$spacing16">
<Icon
color={colors.accent1.get()}
height={iconSize * iconScale}
strokeWidth={2}
width={iconSize * iconScale}
/>
</AnimatedFlex>
</TouchableArea>
</Trace>
)
}
...@@ -29,6 +29,7 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs' ...@@ -29,6 +29,7 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { useTimeout } from 'utilities/src/time/timing' import { useTimeout } from 'utilities/src/time/timing'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
...@@ -249,7 +250,9 @@ export function OnDeviceRecoveryScreen({ ...@@ -249,7 +250,9 @@ export function OnDeviceRecoveryScreen({
</Flex> </Flex>
</Flex> </Flex>
<WarningModal <WarningModal
caption={t('onboarding.import.onDeviceRecovery.warning.caption')} caption={t('onboarding.import.onDeviceRecovery.warning.caption', {
cloudProvider: getCloudProviderName(),
})}
rejectText={t('common.button.back')} rejectText={t('common.button.back')}
acknowledgeText={t('common.button.continue')} acknowledgeText={t('common.button.continue')}
icon={<PapersText color={colors.neutral1.get()} size="$icon.20" strokeWidth={1.5} />} icon={<PapersText color={colors.neutral1.get()} size="$icon.20" strokeWidth={1.5} />}
......
...@@ -111,6 +111,7 @@ export function SettingsScreen(): JSX.Element { ...@@ -111,6 +111,7 @@ export function SettingsScreen(): JSX.Element {
const fireAnalytic = (): void => const fireAnalytic = (): void =>
sendAnalyticsEvent(WalletEventName.TestnetModeToggled, { sendAnalyticsEvent(WalletEventName.TestnetModeToggled, {
enabled: newIsTestnetMode, enabled: newIsTestnetMode,
location: 'settings',
}) })
setTimeout(() => { setTimeout(() => {
......
import { useFocusEffect, useNavigation } from '@react-navigation/core' import { useNavigation } from '@react-navigation/core'
import { NativeStackScreenProps } from '@react-navigation/native-stack' import { NativeStackScreenProps } from '@react-navigation/native-stack'
import React, { useCallback, useEffect, useState } from 'react' import React, { memo, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ListRenderItemInfo, SectionList } from 'react-native' import { ListRenderItemInfo, SectionList } from 'react-native'
import { SvgProps } from 'react-native-svg' import { SvgProps } from 'react-native-svg'
...@@ -20,12 +20,7 @@ import { ...@@ -20,12 +20,7 @@ import {
import { BackHeader } from 'src/components/layout/BackHeader' import { BackHeader } from 'src/components/layout/BackHeader'
import { Screen } from 'src/components/layout/Screen' import { Screen } from 'src/components/layout/Screen'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { promptPushPermission } from 'src/features/notifications/Onesignal' import { useNotificationToggle } from 'src/features/notifications/hooks/useNotificationsToggle'
import {
NotificationPermission,
useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen'
import { Button, Flex, Switch, Text, useSporeColors } from 'ui/src' import { Button, Flex, Switch, Text, useSporeColors } from 'ui/src'
import NotificationIcon from 'ui/src/assets/icons/bell.svg' import NotificationIcon from 'ui/src/assets/icons/bell.svg'
import GlobalIcon from 'ui/src/assets/icons/global.svg' import GlobalIcon from 'ui/src/assets/icons/global.svg'
...@@ -40,11 +35,14 @@ import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' ...@@ -40,11 +35,14 @@ import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { useAccounts } from 'wallet/src/features/wallet/hooks'
import { useAccounts, useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks'
type Props = NativeStackScreenProps<SettingsStackParamList, MobileScreens.SettingsWallet> type Props = NativeStackScreenProps<SettingsStackParamList, MobileScreens.SettingsWallet>
const onPermissionChanged = (enabled: boolean): void => {
sendAnalyticsEvent(MobileEventName.NotificationsToggled, { enabled })
}
// Specific design request not in standard sizing type // Specific design request not in standard sizing type
const UNICON_ICON_SIZE = 56 const UNICON_ICON_SIZE = 56
...@@ -64,10 +62,6 @@ export function SettingsWallet({ ...@@ -64,10 +62,6 @@ export function SettingsWallet({
const readonly = currentAccount?.type === AccountType.Readonly const readonly = currentAccount?.type === AccountType.Readonly
const navigation = useNavigation<SettingsStackNavigationProp & OnboardingStackNavigationProp>() const navigation = useNavigation<SettingsStackNavigationProp & OnboardingStackNavigationProp>()
const notificationOSPermission = useNotificationOSPermissionsEnabled()
const notificationsEnabledOnFirebase = useSelectAccountNotificationSetting(address)
const [notificationSwitchEnabled, setNotificationSwitchEnabled] = useState<boolean>(notificationsEnabledOnFirebase)
const showEditProfile = !readonly const showEditProfile = !readonly
useEffect(() => { useEffect(() => {
...@@ -77,47 +71,6 @@ export function SettingsWallet({ ...@@ -77,47 +71,6 @@ export function SettingsWallet({
} }
}, [currentAccount, navigation]) }, [currentAccount, navigation])
// Need to trigger a state update when the user backgrounds the app to enable notifications and then returns to this screen
useFocusEffect(
useCallback(
() =>
setNotificationSwitchEnabled(
notificationsEnabledOnFirebase && notificationOSPermission === NotificationPermission.Enabled,
),
[notificationOSPermission, notificationsEnabledOnFirebase],
),
)
const onChangeNotificationSettings = async (enabled: boolean): Promise<void> => {
sendAnalyticsEvent(MobileEventName.NotificationsToggled, { enabled })
if (notificationOSPermission === NotificationPermission.Enabled) {
dispatch(
editAccountActions.trigger({
type: EditAccountAction.TogglePushNotification,
enabled,
address,
}),
)
setNotificationSwitchEnabled(enabled)
} else {
const arePushNotificationsEnabled = await promptPushPermission()
if (arePushNotificationsEnabled) {
dispatch(
editAccountActions.trigger({
type: EditAccountAction.TogglePushNotification,
enabled: true,
address,
}),
)
setNotificationSwitchEnabled(enabled)
} else {
showNotificationSettingsAlert()
}
}
}
const iconProps: SvgProps = { const iconProps: SvgProps = {
color: colors.neutral2.get(), color: colors.neutral2.get(),
height: iconSizes.icon24, height: iconSizes.icon24,
...@@ -141,14 +94,7 @@ export function SettingsWallet({ ...@@ -141,14 +94,7 @@ export function SettingsWallet({
data: [ data: [
...(showEditProfile ? [] : [editNicknameSectionOption]), ...(showEditProfile ? [] : [editNicknameSectionOption]),
{ {
action: ( action: <NotificationsSwitch address={address} />,
<Switch
checked={notificationSwitchEnabled}
disabled={notificationOSPermission === NotificationPermission.Loading}
variant="branded"
onCheckedChange={onChangeNotificationSettings}
/>
),
text: t('settings.setting.wallet.notifications.title'), text: t('settings.setting.wallet.notifications.title'),
icon: <NotificationIcon {...iconProps} />, icon: <NotificationIcon {...iconProps} />,
}, },
...@@ -267,3 +213,13 @@ function AddressDisplayHeader({ address }: { address: Address }): JSX.Element { ...@@ -267,3 +213,13 @@ function AddressDisplayHeader({ address }: { address: Address }): JSX.Element {
</Flex> </Flex>
) )
} }
const NotificationsSwitch: React.FC<{ address: Address }> = memo(({ address }) => {
const { isEnabled, isPending, toggle } = useNotificationToggle({
address,
onPermissionChanged,
})
return <Switch checked={isEnabled} disabled={isPending} variant="branded" onCheckedChange={toggle} />
})
NotificationsSwitch.displayName = 'NotificationsSwitch'
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
import React, { PropsWithChildren } from 'react' import React, { PropsWithChildren } from 'react'
import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider'
import type { MobileState } from 'src/app/mobileReducer' import type { MobileState } from 'src/app/mobileReducer'
import { navigationRef } from 'src/app/navigation/NavigationContainer' import { navigationRef } from 'src/app/navigation/navigationRef'
import { store as appStore, persistedReducer } from 'src/app/store' import { store as appStore, persistedReducer } from 'src/app/store'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
......
...@@ -13,6 +13,7 @@ ignores: [ ...@@ -13,6 +13,7 @@ ignores: [
'process', 'process',
'madge', 'madge',
# Dependencies that depcheck thinks are missing but are actually present or never used # Dependencies that depcheck thinks are missing but are actually present or never used
'stories',
## package.json scripts ## package.json scripts
'esbuild-register', 'esbuild-register',
## GraphQL ## GraphQL
...@@ -30,6 +31,17 @@ ignores: [ ...@@ -30,6 +31,17 @@ ignores: [
'@babel/preset-env', '@babel/preset-env',
'eslint-plugin-import', 'eslint-plugin-import',
'terser-webpack-plugin', 'terser-webpack-plugin',
## Storybook
'@svgr/webpack',
'ts-loader',
'@chromatic-com/storybook',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-onboarding',
'@storybook/blocks',
'@storybook/preset-create-react-app',
'eslint-plugin-storybook',
'prop-types',
## Testing ## Testing
'@types/testing-library__cypress', '@types/testing-library__cypress',
## i18n ## i18n
......
...@@ -15,6 +15,7 @@ REACT_APP_QUICKNODE_BASE_RPC_URL="" ...@@ -15,6 +15,7 @@ REACT_APP_QUICKNODE_BASE_RPC_URL=""
REACT_APP_QUICKNODE_BLAST_RPC_URL="" REACT_APP_QUICKNODE_BLAST_RPC_URL=""
REACT_APP_QUICKNODE_BNB_RPC_URL="" REACT_APP_QUICKNODE_BNB_RPC_URL=""
REACT_APP_QUICKNODE_CELO_RPC_URL="" REACT_APP_QUICKNODE_CELO_RPC_URL=""
REACT_APP_QUICKNODE_MONAD_TESTNET_RPC_URL=""
REACT_APP_QUICKNODE_OP_RPC_URL="" REACT_APP_QUICKNODE_OP_RPC_URL=""
REACT_APP_QUICKNODE_POLYGON_RPC_URL="" REACT_APP_QUICKNODE_POLYGON_RPC_URL=""
REACT_APP_MOONPAY_API="https://api.moonpay.com" REACT_APP_MOONPAY_API="https://api.moonpay.com"
......
...@@ -3,3 +3,4 @@ babel.config.js ...@@ -3,3 +3,4 @@ babel.config.js
jest.config.js jest.config.js
metro.config.js metro.config.js
node_modules node_modules
.storybook/stories
...@@ -8,7 +8,7 @@ rulesDirPlugin.RULES_DIR = 'eslint_rules' ...@@ -8,7 +8,7 @@ rulesDirPlugin.RULES_DIR = 'eslint_rules'
module.exports = { module.exports = {
root: true, root: true,
extends: ['@uniswap/eslint-config/react'], extends: ['@uniswap/eslint-config/react', 'plugin:storybook/recommended'],
plugins: ['rulesdir'], plugins: ['rulesdir'],
rules: { rules: {
......
...@@ -54,3 +54,5 @@ cypress/screenshots ...@@ -54,3 +54,5 @@ cypress/screenshots
.vercel .vercel
.wrangler .wrangler
*storybook.log
import type { StorybookConfig } from '@storybook/react-webpack5'
import { dirname, join, resolve } from 'path'
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')))
}
const config: StorybookConfig = {
stories: ['../../../packages/ui/**/*.stories.?(ts|tsx)', '../../../packages/ui/**/*.mdx'],
addons: [
getAbsolutePath('@storybook/preset-create-react-app'),
getAbsolutePath('@storybook/addon-onboarding'),
getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@chromatic-com/storybook'),
getAbsolutePath('@storybook/addon-interactions'),
],
framework: {
name: getAbsolutePath('@storybook/react-webpack5'),
options: {},
},
staticDirs: ['../public'],
webpackFinal: (config) => {
// This modifies the existing image rule to exclude `.svg` files
// since we handle those with `@svgr/webpack`.
const imageRule =
config?.module?.rules &&
config.module.rules.find((rule) => {
if (rule && typeof rule !== 'string' && rule.test instanceof RegExp) {
return rule.test.test('.svg')
}
return
})
if (imageRule && typeof imageRule !== 'string') {
imageRule.exclude = /\.svg$/i
}
config?.module?.rules &&
config.module.rules.push({
test: /\.svg$/i,
use: ['@svgr/webpack'],
})
config?.module?.rules &&
config.module.rules.push({
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
})
config.resolve ??= {}
config.resolve = {
...config.resolve,
alias: {
...config?.resolve?.alias,
'react-native$': 'react-native-web',
},
}
config.resolve.modules = [resolve(__dirname, '../src'), 'node_modules']
return config
},
}
export default config
import type { Preview } from '@storybook/react'
import { TamaguiProvider } from '../src/theme/tamaguiProvider'
const preview: Preview = {
decorators: [
(Story) => (
<TamaguiProvider>
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
<Story />
</TamaguiProvider>
),
],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
}
export default preview
...@@ -5,7 +5,7 @@ const { execSync } = require('child_process') ...@@ -5,7 +5,7 @@ const { execSync } = require('child_process')
const { readFileSync } = require('fs') const { readFileSync } = require('fs')
const MiniCssExtractPlugin = require('mini-css-extract-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path') const path = require('path')
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin') const ModuleScopePlugin = require(path.resolve(__dirname, '..', '..','node_modules/react-scripts/node_modules/react-dev-utils/ModuleScopePlugin'))
const { IgnorePlugin, ProvidePlugin, DefinePlugin } = require('webpack') const { IgnorePlugin, ProvidePlugin, DefinePlugin } = require('webpack')
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin') const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
......
...@@ -30,7 +30,9 @@ ...@@ -30,7 +30,9 @@
"test:cloud": "yarn jest functions --config=functions/jest.config.json", "test:cloud": "yarn jest functions --config=functions/jest.config.json",
"cypress:open": "cypress open --browser chrome --e2e", "cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e", "cypress:run": "cypress run --browser chrome --e2e",
"deduplicate": "yarn-deduplicate --strategy=highest" "deduplicate": "yarn-deduplicate --strategy=highest",
"storybook:run": "storybook dev -p 6006",
"storybook:build": "storybook build"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
...@@ -70,10 +72,19 @@ ...@@ -70,10 +72,19 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "7.23.3", "@babel/preset-env": "7.23.3",
"@chromatic-com/storybook": "3.2.2",
"@cloudflare/workers-types": "4.20231025.0", "@cloudflare/workers-types": "4.20231025.0",
"@craco/craco": "7.1.0", "@craco/craco": "7.1.0",
"@crowdin/cli": "3.14.0", "@crowdin/cli": "3.14.0",
"@ethersproject/experimental": "5.7.0", "@ethersproject/experimental": "5.7.0",
"@storybook/addon-essentials": "8.4.2",
"@storybook/addon-interactions": "8.4.2",
"@storybook/addon-onboarding": "8.4.2",
"@storybook/blocks": "8.4.2",
"@storybook/preset-create-react-app": "8.4.2",
"@storybook/react": "8.4.2",
"@storybook/react-webpack5": "8.4.2",
"@storybook/test": "8.4.2",
"@swc/core": "1.3.72", "@swc/core": "1.3.72",
"@swc/jest": "0.2.29", "@swc/jest": "0.2.29",
"@swc/plugin-styled-components": "1.5.97", "@swc/plugin-styled-components": "1.5.97",
...@@ -114,12 +125,14 @@ ...@@ -114,12 +125,14 @@
"concurrently": "8.2.2", "concurrently": "8.2.2",
"cypress": "12.17.4", "cypress": "12.17.4",
"cypress-hardhat": "2.5.3", "cypress-hardhat": "2.5.3",
"depcheck": "1.4.7",
"dotenv": "16.0.3", "dotenv": "16.0.3",
"dotenv-cli": "7.1.0", "dotenv-cli": "7.1.0",
"esbuild-register": "3.6.0", "esbuild-register": "3.6.0",
"eslint": "8.44.0", "eslint": "8.44.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-rulesdir": "0.2.2", "eslint-plugin-rulesdir": "0.2.2",
"eslint-plugin-storybook": "0.8.0",
"hardhat": "2.22.16", "hardhat": "2.22.16",
"husky": "8.0.3", "husky": "8.0.3",
"jest": "29.7.0", "jest": "29.7.0",
...@@ -133,11 +146,13 @@ ...@@ -133,11 +146,13 @@
"path-browserify": "1.0.1", "path-browserify": "1.0.1",
"postinstall-postinstall": "2.1.0", "postinstall-postinstall": "2.1.0",
"process": "0.11.10", "process": "0.11.10",
"prop-types": "15.8.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"resize-observer-polyfill": "1.5.1", "resize-observer-polyfill": "1.5.1",
"serve": "14.2.4", "serve": "14.2.4",
"source-map-explorer": "2.5.3", "source-map-explorer": "2.5.3",
"start-server-and-test": "2.0.0", "start-server-and-test": "2.0.0",
"storybook": "8.4.2",
"swc-loader": "0.2.6", "swc-loader": "0.2.6",
"terser": "5.24.0", "terser": "5.24.0",
"terser-webpack-plugin": "5.3.9", "terser-webpack-plugin": "5.3.9",
...@@ -172,6 +187,7 @@ ...@@ -172,6 +187,7 @@
"@sentry/core": "7.80.0", "@sentry/core": "7.80.0",
"@sentry/react": "7.80.0", "@sentry/react": "7.80.0",
"@sentry/types": "7.80.0", "@sentry/types": "7.80.0",
"@svgr/webpack": "8.0.1",
"@tamagui/core": "1.114.4", "@tamagui/core": "1.114.4",
"@tamagui/portal": "1.114.4", "@tamagui/portal": "1.114.4",
"@tamagui/react-native-svg": "1.114.4", "@tamagui/react-native-svg": "1.114.4",
...@@ -189,7 +205,7 @@ ...@@ -189,7 +205,7 @@
"@uniswap/permit2-sdk": "1.3.0", "@uniswap/permit2-sdk": "1.3.0",
"@uniswap/redux-multicall": "1.1.8", "@uniswap/redux-multicall": "1.1.8",
"@uniswap/router-sdk": "1.15.0", "@uniswap/router-sdk": "1.15.0",
"@uniswap/sdk-core": "6.0.0", "@uniswap/sdk-core": "6.1.0",
"@uniswap/smart-order-router": "3.17.3", "@uniswap/smart-order-router": "3.17.3",
"@uniswap/token-lists": "1.0.0-beta.33", "@uniswap/token-lists": "1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18", "@uniswap/uniswapx-sdk": "2.1.0-beta.18",
...@@ -269,6 +285,7 @@ ...@@ -269,6 +285,7 @@
"styled-components": "5.3.11", "styled-components": "5.3.11",
"tamagui": "1.114.4", "tamagui": "1.114.4",
"tiny-invariant": "1.3.1", "tiny-invariant": "1.3.1",
"ts-loader": "9.5.1",
"typed-redux-saga": "1.5.0", "typed-redux-saga": "1.5.0",
"ui": "workspace:^", "ui": "workspace:^",
"uniswap": "workspace:^", "uniswap": "workspace:^",
......
...@@ -7215,4 +7215,134 @@ ...@@ -7215,4 +7215,134 @@
<lastmod>2024-12-06T22:01:49.956Z</lastmod> <lastmod>2024-12-06T22:01:49.956Z</lastmod>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
<url>
<loc>https://app.uniswap.org/explore/pools/ethereum/0xc4ce8e63921b8b6cbdb8fcb6bd64cc701fb926f2</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/ethereum/0x9febc984504356225405e26833608b17719c82ae</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/ethereum/0x59c38b6775ded821f010dbd30ecabdcf84e04756</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/ethereum/0x2aeee741fa1e21120a21e57db9ee545428e683c9</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/ethereum/0x67324985b5014b36b960273353deb3d96f2f18c2</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/arbitrum/0x2aeee741fa1e21120a21e57db9ee545428e683c9</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/arbitrum/0x67324985b5014b36b960273353deb3d96f2f18c2</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/optimism/0x2aeee741fa1e21120a21e57db9ee545428e683c9</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/optimism/0x67324985b5014b36b960273353deb3d96f2f18c2</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/polygon/0xcec31e540163ddf45a394e00b11ae442ddc0d704</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/polygon/0x7f9121b4f4e040fd066e9dc5c250cf9b4338d5bc</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/polygon/0x2aeee741fa1e21120a21e57db9ee545428e683c9</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/polygon/0x67324985b5014b36b960273353deb3d96f2f18c2</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/base/0x316f12517630903035a0e0b4d6e617593ee432ba</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/base/0x61928bf5f2895b682ecc9b13957aa5a5fe040cc0</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/base/0x9399da51c1a85e64cce4b30b554875d2b89b2445</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/base/0x0962a51e121aa8371cd4bb0458b7e5a08c1cbd29</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/base/0xfbb6eed8e7aa03b138556eedaf5d271a5e1e43ef</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/base/0x2aeee741fa1e21120a21e57db9ee545428e683c9</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/base/0x67324985b5014b36b960273353deb3d96f2f18c2</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/bnb/0x913a4ed1636c474e6451b5e9249d94046a24bb33</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/bnb/0x8e3ecc0b261f1a4db62321090575eb299844f077</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/bnb/0x2aeee741fa1e21120a21e57db9ee545428e683c9</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/bnb/0x67324985b5014b36b960273353deb3d96f2f18c2</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/celo/0x2aeee741fa1e21120a21e57db9ee545428e683c9</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/pools/celo/0x67324985b5014b36b960273353deb3d96f2f18c2</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
</urlset> </urlset>
\ No newline at end of file
...@@ -8385,4 +8385,249 @@ ...@@ -8385,4 +8385,249 @@
<lastmod>2024-12-07T02:42:16.799Z</lastmod> <lastmod>2024-12-07T02:42:16.799Z</lastmod>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0x9cdf242ef7975d8c68d5c1f5b6905801699b1940</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0xadd39272e83895e7d3f244f696b7a25635f34234</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0x0000000000c5dc95539589fbd24be07c6c14eca4</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0xcab254f1a32343f11ab41fbde90ecb410cde348a</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0x95ed629b028cf6aadd1408bb988c6d1daabe4767</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0xa6c0c097741d55ecd9a3a7def3a8253fd022ceb9</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0xd888a5460fffa4b14340dd9fe2710cbabd520659</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0x683989afc948477fd38567f8327f501562c955ac</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0x946fb08103b400d1c79e07acccdef5cfd26cd374</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0x7076de6ff1d91e00be7e92458089c833de99e22e</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0xadf7c35560035944e805d98ff17d58cde2449389</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0x62b9c7356a2dc64a1969e19c23e4f579f9810aa7</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0xd19b72e027cd66bde41d8f60a13740a26c4be8f3</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0x66a1e37c9b0eaddca17d3662d6c05f4decf3e110</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/ethereum/0xf477ac7719e2e659001455cdda0cc8f3ad10b604</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/arbitrum/0xe16e2548a576ad448fb014bbe85284d7f3542df5</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/arbitrum/0x330bd769382cfc6d50175903434ccc8d206dcae5</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/arbitrum/0xb08d8becab1bf76a9ce3d2d5fa946f65ec1d3e83</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/optimism/0x8b21e9b7daf2c4325bf3d18c1beb79a347fe902a</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/polygon/0xd9a9b4d466747e1ebcb7aeb42784452f40452367</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/polygon/0x306fd3e7b169aa4ee19412323e1a5995b8c1a1f4</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/polygon/0x9cb74c8032b007466865f060ad2c46145d45553d</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x333333c465a19c85f85c6cfbed7b16b0b26e3333</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xb9a5f238dc61eebe820060226c8143cd24624771</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xc48cddc6f2650bdb13dcf6681f61ba07209b5299</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xfb42da273158b0f642f59f2ba7cc1d5457481677</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x6f35720b272bf23832852b13ae9888c706e1a379</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x4498cd8ba045e00673402353f5a4347562707e7d</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x1035ae3f87a91084c6c5084d0615cc6121c5e228</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x3639e6f4c224ebd1bf6373c3d97917d33e0492bb</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x8bfac1b375bf2894d6f12fb2eb48b1c1a7916789</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xd27c288fd69f228e0c02f79e5ecadff962e05a2b</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x62ff28a01abd2484adb18c61f78f30fb2e4a6fdb</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x315b8c9a1123c10228d469551033440441b41f0b</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x1d008f50fb828ef9debbbeae1b71fffe929bf317</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xeec468333ccc16d4bf1cef497a56cf8c0aae4ca3</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x20dd04c17afd5c9a8b3f2cdacaa8ee7907385bef</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x25e0a7767d03461eaf88b47cd9853722fe05dfd3</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x22af33fe49fd1fa80c7149773dde5890d3c76f3b</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xbc1852f8940991d91bd2b09a5abb5e7b8092a16c</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x06a63c498ef95ad1fa4fff841955e512b4b2198a</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xeb6d78148f001f3aa2f588997c5e102e489ad341</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xa4dc5a82839a148ff172b5b8ba9d52e681fd2261</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xef22cb48b8483df6152e1423b19df5553bbd818b</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0xf83759099dc88f75fc83de854c41e0d9e83ada9b</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/base/0x844c03892863b0e3e00e805e41b34527044d5c72</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/bnb/0xc9de725a4be9ab74b136c29d4731d6bebd7122e8</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/bnb/0x59e69094398afbea632f8bd63033bdd2443a3be1</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://app.uniswap.org/explore/tokens/bnb/0x15f9eb4b9beafa9db35341c5694c0b6573809808</loc>
<lastmod>2024-12-13T21:22:32.347Z</lastmod>
<priority>0.8</priority>
</url>
</urlset> </urlset>
\ No newline at end of file
...@@ -7,42 +7,6 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` ...@@ -7,42 +7,6 @@ exports[`CancelOrdersDialog should render limit order text 1`] = `
min-width: 0; min-width: 0;
} }
.c7 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 4px;
}
.c18 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
gap: 12px;
}
.c12 { .c12 {
color: #222222; color: #222222;
-webkit-letter-spacing: -0.01em; -webkit-letter-spacing: -0.01em;
...@@ -226,6 +190,42 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` ...@@ -226,6 +190,42 @@ exports[`CancelOrdersDialog should render limit order text 1`] = `
background-color: transparent; background-color: transparent;
} }
.c7 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 4px;
}
.c18 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
gap: 12px;
}
.c5 { .c5 {
width: -webkit-fit-content; width: -webkit-fit-content;
width: -moz-fit-content; width: -moz-fit-content;
...@@ -626,42 +626,6 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` ...@@ -626,42 +626,6 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = `
min-width: 0; min-width: 0;
} }
.c7 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 4px;
}
.c18 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
gap: 12px;
}
.c12 { .c12 {
color: #222222; color: #222222;
-webkit-letter-spacing: -0.01em; -webkit-letter-spacing: -0.01em;
...@@ -845,6 +809,42 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` ...@@ -845,6 +809,42 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = `
background-color: transparent; background-color: transparent;
} }
.c7 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 4px;
}
.c18 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
gap: 12px;
}
.c5 { .c5 {
width: -webkit-fit-content; width: -webkit-fit-content;
width: -moz-fit-content; width: -moz-fit-content;
......
...@@ -7,42 +7,6 @@ exports[`OrderContent should render without error, filled order 1`] = ` ...@@ -7,42 +7,6 @@ exports[`OrderContent should render without error, filled order 1`] = `
min-width: 0; min-width: 0;
} }
.c2 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c11 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 12px;
}
.c6 { .c6 {
color: #222222; color: #222222;
-webkit-letter-spacing: -0.01em; -webkit-letter-spacing: -0.01em;
...@@ -93,6 +57,42 @@ exports[`OrderContent should render without error, filled order 1`] = ` ...@@ -93,6 +57,42 @@ exports[`OrderContent should render without error, filled order 1`] = `
background-color: #22222212; background-color: #22222212;
} }
.c2 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c11 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 12px;
}
.c0 { .c0 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
...@@ -463,42 +463,6 @@ exports[`OrderContent should render without error, limit order 1`] = ` ...@@ -463,42 +463,6 @@ exports[`OrderContent should render without error, limit order 1`] = `
min-width: 0; min-width: 0;
} }
.c2 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c11 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 12px;
}
.c6 { .c6 {
color: #222222; color: #222222;
-webkit-letter-spacing: -0.01em; -webkit-letter-spacing: -0.01em;
...@@ -621,6 +585,42 @@ exports[`OrderContent should render without error, limit order 1`] = ` ...@@ -621,6 +585,42 @@ exports[`OrderContent should render without error, limit order 1`] = `
background-color: transparent; background-color: transparent;
} }
.c2 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c11 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 12px;
}
.c0 { .c0 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
...@@ -1035,42 +1035,6 @@ exports[`OrderContent should render without error, open order 1`] = ` ...@@ -1035,42 +1035,6 @@ exports[`OrderContent should render without error, open order 1`] = `
min-width: 0; min-width: 0;
} }
.c2 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c11 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 12px;
}
.c6 { .c6 {
color: #222222; color: #222222;
-webkit-letter-spacing: -0.01em; -webkit-letter-spacing: -0.01em;
...@@ -1174,6 +1138,42 @@ exports[`OrderContent should render without error, open order 1`] = ` ...@@ -1174,6 +1138,42 @@ exports[`OrderContent should render without error, open order 1`] = `
background-color: transparent; background-color: transparent;
} }
.c2 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c11 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 12px;
}
.c0 { .c0 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
......
...@@ -125,6 +125,11 @@ function callsV4PositionManagerContract(assetActivity: TransactionActivity) { ...@@ -125,6 +125,11 @@ function callsV4PositionManagerContract(assetActivity: TransactionActivity) {
return false return false
} }
// monad testnet does not have v4 support
if (supportedChain === UniverseChainId.MonadTestnet) {
return false
}
return isSameAddress(assetActivity.details.to, CHAIN_TO_ADDRESSES_MAP[supportedChain].v4PositionManagerAddress) return isSameAddress(assetActivity.details.to, CHAIN_TO_ADDRESSES_MAP[supportedChain].v4PositionManagerAddress)
} }
function callsPositionManagerContract(assetActivity: TransactionActivity) { function callsPositionManagerContract(assetActivity: TransactionActivity) {
......
...@@ -7,6 +7,29 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = ` ...@@ -7,6 +7,29 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = `
min-width: 0; min-width: 0;
} }
.c5 {
color: #7D7D7D;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
letter-spacing: -0.01em;
}
.c10 {
color: #222222;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
letter-spacing: -0.01em;
}
.c11 {
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
letter-spacing: -0.01em;
}
.c1 { .c1 {
width: 100%; width: 100%;
display: -webkit-box; display: -webkit-box;
...@@ -42,29 +65,6 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = ` ...@@ -42,29 +65,6 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = `
gap: 4px; gap: 4px;
} }
.c5 {
color: #7D7D7D;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
letter-spacing: -0.01em;
}
.c10 {
color: #222222;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
letter-spacing: -0.01em;
}
.c11 {
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
letter-spacing: -0.01em;
}
.c6 { .c6 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
......
...@@ -39,6 +39,95 @@ exports[`AccountDrawer tests AccountDrawer default styles 1`] = ` ...@@ -39,6 +39,95 @@ exports[`AccountDrawer tests AccountDrawer default styles 1`] = `
flex: 1; flex: 1;
} }
.c36 {
color: #7D7D7D;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
letter-spacing: -0.01em;
}
.c37 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
color: #FC72FF;
stroke: #FC72FF;
font-weight: 500;
}
.c37:hover {
opacity: 0.6;
}
.c37:active {
opacity: 0.4;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c10 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 16px;
}
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c26 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
.c8 { .c8 {
width: 100%; width: 100%;
display: -webkit-box; display: -webkit-box;
...@@ -158,95 +247,6 @@ exports[`AccountDrawer tests AccountDrawer default styles 1`] = ` ...@@ -158,95 +247,6 @@ exports[`AccountDrawer tests AccountDrawer default styles 1`] = `
margin: !important; margin: !important;
} }
.c36 {
color: #7D7D7D;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
letter-spacing: -0.01em;
}
.c37 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
color: #FC72FF;
stroke: #FC72FF;
font-weight: 500;
}
.c37:hover {
opacity: 0.6;
}
.c37:active {
opacity: 0.4;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c10 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 16px;
}
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
}
.c26 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 12px;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
.c32 { .c32 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
......
import { RowBetween } from 'components/deprecated/Row'
import styled, { DefaultTheme } from 'lib/styled-components' import styled, { DefaultTheme } from 'lib/styled-components'
import { darken } from 'polished' import { darken } from 'polished'
import { forwardRef } from 'react' import { forwardRef } from 'react'
import { ChevronDown } from 'react-feather'
import { ButtonProps as ButtonPropsOriginal, Button as RebassButton } from 'rebass/styled-components' import { ButtonProps as ButtonPropsOriginal, Button as RebassButton } from 'rebass/styled-components'
export { default as LoadingButtonSpinner } from './LoadingButtonSpinner' export { default as LoadingButtonSpinner } from './LoadingButtonSpinner'
...@@ -306,17 +304,6 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & BaseButton ...@@ -306,17 +304,6 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & BaseButton
} }
} }
export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
return (
<ButtonOutlined {...rest} disabled={disabled}>
<RowBetween>
<div style={{ display: 'flex', alignItems: 'center' }}>{children}</div>
<ChevronDown size={24} />
</RowBetween>
</ButtonOutlined>
)
}
export enum ButtonSize { export enum ButtonSize {
small, small,
medium, medium,
......
import { Currency } from '@uniswap/sdk-core' import { Currency, Percent } from '@uniswap/sdk-core'
import { AxisRight } from 'components/Charts/ActiveLiquidityChart/AxisRight' import { AxisRight } from 'components/Charts/ActiveLiquidityChart/AxisRight'
import { Brush2 } from 'components/Charts/ActiveLiquidityChart/Brush2' import { Brush2 } from 'components/Charts/ActiveLiquidityChart/Brush2'
import { HorizontalArea } from 'components/Charts/ActiveLiquidityChart/HorizontalArea' import { HorizontalArea } from 'components/Charts/ActiveLiquidityChart/HorizontalArea'
...@@ -7,7 +7,9 @@ import { TickTooltip } from 'components/Charts/ActiveLiquidityChart/TickTooltip' ...@@ -7,7 +7,9 @@ import { TickTooltip } from 'components/Charts/ActiveLiquidityChart/TickTooltip'
import { ChartEntry } from 'components/LiquidityChartRangeInput/types' import { ChartEntry } from 'components/LiquidityChartRangeInput/types'
import { max as getMax, scaleLinear } from 'd3' import { max as getMax, scaleLinear } from 'd3'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useSporeColors } from 'ui/src' import { Flex, Text, useSporeColors } from 'ui/src'
import { opacify } from 'ui/src/theme'
import { useFormatter } from 'utils/formatNumbers'
const xAccessor = (d: ChartEntry) => d.activeLiquidity const xAccessor = (d: ChartEntry) => d.activeLiquidity
const yAccessor = (d: ChartEntry) => d.price0 const yAccessor = (d: ChartEntry) => d.price0
...@@ -53,6 +55,11 @@ function findClosestElementBinarySearch(data: ChartEntry[], target?: number) { ...@@ -53,6 +55,11 @@ function findClosestElementBinarySearch(data: ChartEntry[], target?: number) {
return closestElement return closestElement
} }
function scaleToInteger(a: number, precision = 18) {
const scaleFactor = Math.pow(10, precision)
return Math.round(a * scaleFactor)
}
/** /**
* A horizontal version of the active liquidity area chart, which uses the * A horizontal version of the active liquidity area chart, which uses the
* x-y coordinate plane to show the data, but with the axes flipped so lower * x-y coordinate plane to show the data, but with the axes flipped so lower
...@@ -69,6 +76,8 @@ export function ActiveLiquidityChart2({ ...@@ -69,6 +76,8 @@ export function ActiveLiquidityChart2({
brushDomain, brushDomain,
onBrushDomainChange, onBrushDomainChange,
disableBrushInteraction, disableBrushInteraction,
showDiffIndicators,
isMobile,
}: { }: {
id?: string id?: string
currency0: Currency currency0: Currency
...@@ -80,10 +89,13 @@ export function ActiveLiquidityChart2({ ...@@ -80,10 +89,13 @@ export function ActiveLiquidityChart2({
max?: number max?: number
} }
disableBrushInteraction?: boolean disableBrushInteraction?: boolean
showDiffIndicators?: boolean
dimensions: { width: number; height: number; contentWidth: number; axisLabelPaneWidth: number } dimensions: { width: number; height: number; contentWidth: number; axisLabelPaneWidth: number }
brushDomain?: [number, number] brushDomain?: [number, number]
onBrushDomainChange: (domain: [number, number], mode: string | undefined) => void onBrushDomainChange: (domain: [number, number], mode: string | undefined) => void
isMobile?: boolean
}) { }) {
const { formatPercent } = useFormatter()
const colors = useSporeColors() const colors = useSporeColors()
const svgRef = useRef<SVGSVGElement | null>(null) const svgRef = useRef<SVGSVGElement | null>(null)
const [hoverY, setHoverY] = useState<number>() const [hoverY, setHoverY] = useState<number>()
...@@ -124,6 +136,9 @@ export function ActiveLiquidityChart2({ ...@@ -124,6 +136,9 @@ export function ActiveLiquidityChart2({
} }
}, [brushDomain, onBrushDomainChange, yScale]) }, [brushDomain, onBrushDomainChange, yScale])
const southHandleInView = brushDomain && yScale(brushDomain[0]) >= 0 && yScale(brushDomain[0]) <= height
const northHandleInView = brushDomain && yScale(brushDomain[1]) >= 0 && yScale(brushDomain[1]) <= height
return ( return (
<> <>
{hoverY && hoveredTick && ( {hoverY && hoveredTick && (
...@@ -138,6 +153,42 @@ export function ActiveLiquidityChart2({ ...@@ -138,6 +153,42 @@ export function ActiveLiquidityChart2({
currency1={currency1} currency1={currency1}
/> />
)} )}
{showDiffIndicators && (
<>
{southHandleInView && (
<Flex
borderRadius="$rounded12"
backgroundColor="$surface2"
borderColor="$surface3"
borderWidth={1}
p="$padding8"
position="absolute"
left={0}
top={yScale(brushDomain[0]) - 16}
>
<Text variant="body4">
{formatPercent(new Percent(scaleToInteger(brushDomain[0] - current), scaleToInteger(current)))}
</Text>
</Flex>
)}
{northHandleInView && (
<Flex
borderRadius="$rounded12"
backgroundColor="$surface2"
borderColor="$surface3"
borderWidth={1}
p="$padding8"
position="absolute"
left={0}
top={yScale(brushDomain[1]) - 16}
>
<Text variant="body4">
{formatPercent(new Percent(scaleToInteger(brushDomain[1] - current), scaleToInteger(current)))}
</Text>
</Flex>
)}
</>
)}
<svg <svg
ref={svgRef} ref={svgRef}
width="100%" width="100%"
...@@ -186,8 +237,8 @@ export function ActiveLiquidityChart2({ ...@@ -186,8 +237,8 @@ export function ActiveLiquidityChart2({
xValue={xAccessor} xValue={xAccessor}
yValue={yAccessor} yValue={yAccessor}
brushDomain={brushDomain} brushDomain={brushDomain}
fill={brushDomain ? colors.neutral1.val : colors.accent1.val} fill={opacify(isMobile ? 10 : 100, brushDomain ? colors.neutral1.val : colors.accent1.val)}
selectedFill={colors.accent1.val} selectedFill={opacify(isMobile ? 10 : 100, colors.accent1.val)}
containerHeight={height} containerHeight={height}
containerWidth={width - axisLabelPaneWidth} containerWidth={width - axisLabelPaneWidth}
/> />
...@@ -209,14 +260,16 @@ export function ActiveLiquidityChart2({ ...@@ -209,14 +260,16 @@ export function ActiveLiquidityChart2({
/> />
)} )}
<AxisRight {isMobile ? null : (
yScale={yScale} <AxisRight
offset={width - contentWidth} yScale={yScale}
min={brushDomain?.[0]} offset={width - contentWidth}
current={current} min={brushDomain?.[0]}
max={brushDomain?.[1]} current={current}
height={height} max={brushDomain?.[1]}
/> height={height}
/>
)}
</g> </g>
<Brush2 <Brush2
......
import { NumberValue, ScaleLinear, axisRight, Axis as d3Axis, select } from 'd3' import { NumberValue, ScaleLinear, axisRight, Axis as d3Axis, select } from 'd3'
import styled from 'lib/styled-components' import styled from 'lib/styled-components'
import { useMemo } from 'react' import { useMemo } from 'react'
import { NumberType, useFormatter } from 'utils/formatNumbers'
const StyledGroup = styled.g` const StyledGroup = styled.g`
line { line {
...@@ -12,7 +13,7 @@ const StyledGroup = styled.g` ...@@ -12,7 +13,7 @@ const StyledGroup = styled.g`
} }
` `
const TEXT_Y_OFFSET = 10 const TEXT_Y_OFFSET = 5
const Axis = ({ const Axis = ({
axisGenerator, axisGenerator,
...@@ -61,6 +62,7 @@ export const AxisRight = ({ ...@@ -61,6 +62,7 @@ export const AxisRight = ({
current?: number current?: number
max?: number max?: number
}) => { }) => {
const { formatNumber } = useFormatter()
const tickValues = useMemo(() => { const tickValues = useMemo(() => {
const minCoordinate = min ? yScale(min) : undefined const minCoordinate = min ? yScale(min) : undefined
const maxCoordinate = max ? yScale(max) : undefined const maxCoordinate = max ? yScale(max) : undefined
...@@ -76,7 +78,18 @@ export const AxisRight = ({ ...@@ -76,7 +78,18 @@ export const AxisRight = ({
return ( return (
<StyledGroup transform={`translate(${offset}, 0)`}> <StyledGroup transform={`translate(${offset}, 0)`}>
<Axis axisGenerator={axisRight(yScale).tickValues(tickValues)} height={height} yScale={yScale} /> <Axis
axisGenerator={axisRight(yScale)
.tickValues(tickValues)
.tickFormat((d) =>
formatNumber({
input: d as number,
type: NumberType.TokenQuantityStats,
}),
)}
height={height}
yScale={yScale}
/>
</StyledGroup> </StyledGroup>
) )
} }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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