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:
- CIDv0: `QmYVT8bXNHTgPdSTegLQ62Z6f7E51fQbeMvn2nJAxMRGBE`
- CIDv1: `bafybeiew2yawtry3lld6g5ankyq3bsbvjfchxxhboph45u6os6t2babtru`
- CIDv0: `QmP8VRKK9FDri6XhqC8xNCXJTRqbfWmiF7jTdaDBJbt3uv`
- CIDv1: `bafybeialxy3kyi4xjtgo4u3a6k7gpt76j3y77m3cucofzbjxlegxfzufpu`
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.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeiew2yawtry3lld6g5ankyq3bsbvjfchxxhboph45u6os6t2babtru.ipfs.dweb.link/
- https://bafybeiew2yawtry3lld6g5ankyq3bsbvjfchxxhboph45u6os6t2babtru.ipfs.cf-ipfs.com/
- [ipfs://QmYVT8bXNHTgPdSTegLQ62Z6f7E51fQbeMvn2nJAxMRGBE/](ipfs://QmYVT8bXNHTgPdSTegLQ62Z6f7E51fQbeMvn2nJAxMRGBE/)
- https://bafybeialxy3kyi4xjtgo4u3a6k7gpt76j3y77m3cucofzbjxlegxfzufpu.ipfs.dweb.link/
- https://bafybeialxy3kyi4xjtgo4u3a6k7gpt76j3y77m3cucofzbjxlegxfzufpu.ipfs.cf-ipfs.com/
- [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
* **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
\ No newline at end of file
web/5.63.0
\ No newline at end of file
ignores: [
# Dependencies that depcheck thinks are unused but are actually used
"react-native-web",
"jest-environment-jsdom",
"webpack-cli",
'react-native-web',
'jest-environment-jsdom',
'webpack-cli',
# Dependencies that depcheck thinks are missing but are actually present or never used
## Internal packages / workspaces
"src",
"tsconfig",
'src',
'tsconfig',
# Webpack plugins
"@svgr/webpack",
"tamagui-loader",
"esbuild-loader",
"swc-loader",
'@svgr/webpack',
'tamagui-loader',
'esbuild-loader',
'style-loader',
'css-loader',
'swc-loader',
## Testing
"@testing-library/dom",
'@testing-library/dom',
]
......@@ -67,6 +67,7 @@
"clean-webpack-plugin": "4.0.0",
"concurrently": "8.2.2",
"copy-webpack-plugin": "11.0.0",
"css-loader": "6.11.0",
"esbuild-loader": "3.2.0",
"eslint": "8.44.0",
"jest": "29.7.0",
......@@ -77,6 +78,7 @@
"react-refresh": "0.14.0",
"serve": "14.2.4",
"statsig-js": "4.41.0",
"style-loader": "3.3.2",
"swc-loader": "0.2.6",
"tamagui-loader": "1.114.4",
"typescript": "5.3.3",
......
......@@ -12,7 +12,7 @@ export async function initializeDatadog(appName: string): Promise<void> {
const datadogEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Datadog))
logger.setWalletDatadogEnabled(datadogEnabled)
if (__DEV__ || !datadogEnabled) {
if (!datadogEnabled) {
return
}
......
......@@ -5,7 +5,7 @@ import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { Flex, Text } from 'ui/src'
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'
export const WrapTransactionDetails = ({
......@@ -31,7 +31,7 @@ export const WrapTransactionDetails = ({
const networkFee = useTransactionGasFee(txRequest)
const { gasFeeFormatted } = useGasFeeFormattedAmounts({
const { gasFeeFormatted } = useGasFeeFormattedDisplayAmounts({
gasFee: networkFee,
chainId,
placeholder: undefined,
......
......@@ -92,6 +92,7 @@ export function SettingsScreen(): JSX.Element {
const fireAnalytic = (): void => {
sendAnalyticsEvent(WalletEventName.TestnetModeToggled, {
enabled: isChecked,
location: 'settings',
})
}
......
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.12.0",
"version": "1.13.0",
"minimum_chrome_version": "116",
"icons": {
"16": "assets/icon16.png",
......
......@@ -12,6 +12,7 @@ import {
v14Schema,
v15Schema,
v16Schema,
v17Schema,
v1Schema,
v2Schema,
v3Schema,
......@@ -48,6 +49,7 @@ import {
testMovedUserSettings,
testRemoveCreatedOnboardingRedesignAccount,
testRemoveHoldToSwap,
testUnchecksumDismissedTokenWarningKeys,
testUpdateExploreOrderByType,
} from 'wallet/src/state/walletMigrationsTests'
......@@ -274,4 +276,8 @@ describe('Redux state migrations', () => {
it('migrates from v16 to v17', async () => {
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/explicit-function-return-type */
import { unchecksumDismissedTokenWarningKeys } from 'uniswap/src/state/uniswapMigrations'
import {
activatePendingAccounts,
addCreatedOnboardingRedesignAccountBehaviorHistory,
......@@ -42,6 +43,7 @@ export const migrations = {
15: moveCurrencySetting,
16: updateExploreOrderByType,
17: removeCreatedOnboardingRedesignAccountBehaviorHistory,
18: unchecksumDismissedTokenWarningKeys,
}
export const EXTENSION_STATE_VERSION = 17
export const EXTENSION_STATE_VERSION = 18
......@@ -31,6 +31,19 @@ const normalizedStories = [
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.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 {
......
......@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
}
def devVersionName = "1.42"
def betaVersionName = "1.42"
def prodVersionName = "1.42"
def devVersionName = "1.43"
def betaVersionName = "1.43"
def prodVersionName = "1.43"
android {
ndkVersion rootProject.ext.ndkVersion
......
......@@ -2204,7 +2204,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2257,7 +2257,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2310,7 +2310,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2363,7 +2363,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2401,7 +2401,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2437,7 +2437,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2472,7 +2472,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2507,7 +2507,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2554,7 +2554,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2600,7 +2600,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
......@@ -2646,7 +2646,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
......@@ -2692,7 +2692,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
......@@ -2734,7 +2734,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2777,7 +2777,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
......@@ -2820,7 +2820,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
......@@ -2863,7 +2863,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
......@@ -2899,7 +2899,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -2937,7 +2937,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3137,7 +3137,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -3181,7 +3181,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
......@@ -3292,7 +3292,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3363,7 +3363,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
......@@ -3474,7 +3474,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3545,7 +3545,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
......@@ -33,7 +33,7 @@
"firestore:deploy:rules": "firebase deploy --only firestore:rules",
"link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 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: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)\"",
......@@ -87,11 +87,12 @@
"@shopify/react-native-performance-navigation": "3.0.0",
"@shopify/react-native-skia": "1.4.2",
"@sparkfabrik/react-native-idfa-aaid": "1.2.0",
"@tanstack/react-query": "5.51.16",
"@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.40.0",
"@uniswap/client-explore": "0.0.12",
"@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/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1",
......
......@@ -15,7 +15,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens'
import { useDispatch, useSelector } from 'react-redux'
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 { AppModals } from 'src/app/modals/AppModals'
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
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 { datadogEnabled, 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
datadogEnabled, // trackInteractions
datadogEnabled, // trackResources
datadogEnabled, // trackErrors
localDevDatadogEnabled ? TrackingConsent.GRANTED : undefined,
)
......
......@@ -83,6 +83,7 @@ import {
v79Schema,
v7Schema,
v80Schema,
v81Schema,
v8Schema,
v9Schema,
} from 'src/app/schema'
......@@ -125,6 +126,7 @@ import {
testMovedUserSettings,
testRemoveCreatedOnboardingRedesignAccount,
testRemoveHoldToSwap,
testUnchecksumDismissedTokenWarningKeys,
testUpdateExploreOrderByType,
} from 'wallet/src/state/walletMigrationsTests'
import { signerMnemonicAccount } from 'wallet/src/test/fixtures'
......@@ -1592,4 +1594,8 @@ describe('Redux state migrations', () => {
it('migrates from v80 to v81', async () => {
testRemoveCreatedOnboardingRedesignAccount(migrations[81], v80Schema)
})
it('migrates from v81 to v82', () => {
testUnchecksumDismissedTokenWarningKeys(migrations[82], v81Schema)
})
})
......@@ -16,6 +16,7 @@ import {
TransactionStatus,
TransactionType,
} from 'uniswap/src/features/transactions/types/transactionDetails'
import { unchecksumDismissedTokenWarningKeys } from 'uniswap/src/state/uniswapMigrations'
import { getNFTAssetKey } from 'wallet/src/features/nfts/utils'
import { Account } from 'wallet/src/features/wallet/accounts/types'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
......@@ -954,6 +955,8 @@ export const migrations = {
80: updateExploreOrderByType,
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
import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal'
import { closeModal } from 'src/features/modals/modalSlice'
import { ScantasticModal } from 'src/features/scantastic/ScantasticModal'
import { TestnetSwitchModal } from 'src/features/testnetMode/TestnetSwitchModal'
import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal'
import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
......@@ -119,6 +120,10 @@ export function AppModals(): JSX.Element {
<LazyModalRenderer name={ModalName.TokenWarning}>
<TokenWarningModalWrapper />
</LazyModalRenderer>
<LazyModalRenderer name={ModalName.TestnetSwitchModal}>
<TestnetSwitchModal />
</LazyModalRenderer>
</>
)
}
import { DdRumReactNavigationTracking } from '@datadog/mobile-react-navigation'
import {
createNavigationContainerRef,
DefaultTheme,
NavigationContainer as NativeNavigationContainer,
NavigationContainerRefWithCurrent,
......@@ -9,6 +8,7 @@ import { SharedEventName } from '@uniswap/analytics-events'
import React, { FC, PropsWithChildren, useCallback, useState } from 'react'
import { Linking } from 'react-native'
import { useDispatch } from 'react-redux'
import { navigationRef } from 'src/app/navigation/navigationRef'
import { RootParamList } from 'src/app/navigation/types'
import { openDeepLink } from 'src/features/deepLinking/handleDeepLinkSaga'
import { DIRECT_LOG_ONLY_SCREENS } from 'src/features/telemetry/directLogScreens'
......@@ -18,6 +18,7 @@ import { useSporeColors } from 'ui/src'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileNavScreen } from 'uniswap/src/types/screens/mobile'
import { datadogEnabled } from 'utilities/src/environment/constants'
import { useAsyncData } from 'utilities/src/react/hooks'
import { sleep } from 'utilities/src/time/timing'
......@@ -25,8 +26,6 @@ interface Props {
onReady?: (navigationRef: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>) => void
}
export const navigationRef = createNavigationContainerRef()
/** Wrapped `NavigationContainer` with telemetry tracing. */
export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, onReady }: PropsWithChildren<Props>) => {
const colors = useSporeColors()
......@@ -54,7 +53,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
const initialRoute = navigationRef.getCurrentRoute()?.name as MobileNavScreen
setRouteName(initialRoute)
if (!__DEV__) {
if (datadogEnabled) {
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 { TransitionPresets, createStackNavigator } from '@react-navigation/stack'
import React, { useEffect } from 'react'
import { DevSettings } from 'react-native'
import { useSelector } from 'react-redux'
import StorybookUIRoot from 'src/../.storybook'
import { navigationRef } from 'src/app/navigation/NavigationContainer'
import { renderHeaderBackButton, renderHeaderBackImage } from 'src/app/navigation/components'
import { navigationRef } from 'src/app/navigation/navigationRef'
import {
AppStackParamList,
AppStackScreenProp,
......@@ -79,6 +85,7 @@ import {
UnitagScreens,
UnitagStackParamList,
} from 'uniswap/src/types/screens/mobile'
import { datadogEnabled } from 'utilities/src/environment/constants'
import { isDevEnv } from 'utilities/src/environment/env'
import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
......@@ -138,6 +145,45 @@ export function WrappedHomeScreen(props: AppStackScreenProp<MobileScreens.Home>)
}
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 {
const colors = useSporeColors()
......@@ -145,7 +191,7 @@ export function ExploreStackNavigator(): JSX.Element {
return (
<NavigationContainer
ref={exploreNavigationRef}
independent={true}
independent
theme={{
dark: false,
colors: {
......@@ -157,6 +203,8 @@ export function ExploreStackNavigator(): JSX.Element {
notification: 'transparent',
},
}}
onStateChange={stopTracking}
onReady={() => startTracking(exploreNavigationRef)}
>
<HorizontalEdgeGestureTarget />
<ExploreStack.Navigator
......@@ -186,7 +234,12 @@ export function ExploreStackNavigator(): JSX.Element {
export function FiatOnRampStackNavigator(): JSX.Element {
return (
<NavigationContainer independent={true}>
<NavigationContainer
ref={fiatOnRampNavigationRef}
independent
onReady={() => startTracking(fiatOnRampNavigationRef)}
onStateChange={stopTracking}
>
<HorizontalEdgeGestureTarget />
<FiatOnRampProvider>
<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 { navigationRef } from 'src/app/navigation/NavigationContainer'
import { navigationRef } from 'src/app/navigation/navigationRef'
import { RootParamList } from 'src/app/navigation/types'
import { logger } from 'utilities/src/logger/logger'
......
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 { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { spacing } from 'ui/src/theme'
......@@ -12,6 +13,11 @@ import { Account } from 'wallet/src/features/wallet/accounts/types'
const ADDRESS_ROW_HEIGHT = 40
interface SortedAddressData {
address: string
balance: number
}
type Portfolio = NonNullable<NonNullable<NonNullable<AccountListQuery['portfolios']>[0]>>
function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element {
......@@ -26,17 +32,27 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele
.filter((portfolio): portfolio is Portfolio => Boolean(portfolio))
.map((portfolio) => ({
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 =
Math.floor((fullHeight * 0.3) / ADDRESS_ROW_HEIGHT) * ADDRESS_ROW_HEIGHT +
ADDRESS_ROW_HEIGHT / 2 +
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 (
<Flex
borderColor="$surface3"
......@@ -46,17 +62,14 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele
px="$spacing12"
width="100%"
>
<ScrollView bounces={false} contentContainerStyle={styles.accounts}>
{sortedAddressesByBalance.map(({ address, balance }, index) => (
<AssociatedAccountRow
address={address}
balance={balance}
index={index}
loading={loading}
totalCount={accounts.length}
<FlatList
data={sortedAddressesByBalance}
keyExtractor={(item) => item.address}
renderItem={renderItem}
bounces={false}
contentContainerStyle={[styles.accounts, { paddingBottom: spacing.spacing12 }]}
keyboardShouldPersistTaps="handled"
/>
))}
</ScrollView>
</Flex>
)
}
......
......@@ -2,7 +2,7 @@ import * as ExpoClipboard from 'expo-clipboard'
import { State } from 'react-native-gesture-handler'
import { fireGestureHandler, getByGestureTestId } from 'react-native-gesture-handler/jest-utils'
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 { fireEvent, render, screen, waitFor, within } from 'src/test/test-utils'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
......
......@@ -5,8 +5,9 @@ import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTim
import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { openModal } from 'src/features/modals/modalSlice'
import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice'
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 { useAvatar } from 'uniswap/src/features/address/avatar'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
......@@ -20,12 +21,16 @@ import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { shortenAddress } from 'utilities/src/addresses'
import { isDevEnv } from 'utilities/src/environment/env'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName'
import useIsFocused from 'wallet/src/features/focus/useIsFocused'
import { useActiveAccount, useActiveAccountAddress, useDisplayName } from 'wallet/src/features/wallet/hooks'
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 isScreenFocused = useIsFocused()
const pressProgress = useSharedValue(0)
......@@ -61,7 +66,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }):
return (
<GestureDetector gesture={tap}>
<Animated.View style={animatedStyle}>
<Settings color="$neutral2" opacity={0.8} size="$icon.24" />
<Settings color="$neutral2" size="$icon.24" />
</Animated.View>
</GestureDetector>
)
......@@ -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 iconSize = 52
......@@ -121,6 +131,7 @@ export function AccountHeader(): JSX.Element {
{activeAddress && (
<Flex alignItems="flex-start" gap="$spacing12" width="100%">
<Flex row justifyContent="space-between" width="100%">
<Flex shrink row gap="$spacing12">
<TouchableArea
alignItems="center"
flexDirection="row"
......@@ -141,31 +152,43 @@ export function AccountHeader(): JSX.Element {
size={iconSize}
/>
</TouchableArea>
<RotatingSettingsIcon onPressSettings={onPressSettings} />
</Flex>
{walletHasName ? (
<Flex
row
alignItems="center"
shrink
alignSelf="center"
gap="$spacing8"
justifyContent="space-between"
testID="account-header/display-name"
>
<TouchableArea flexShrink={1} hitSlop={20} onPress={onPressAccountHeader}>
<TouchableArea flexGrow={1} hitSlop={20} onPress={onPressAccountHeader}>
<AnimatedUnitagDisplayName address={activeAddress} displayName={displayName} />
</TouchableArea>
</Flex>
) : (
<TouchableArea hitSlop={20} testID={TestID.AccountHeaderCopyAddress} onPress={onPressCopyAddress}>
<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="$neutral1" size="$icon.16" />
<CopyAlt color="$neutral2" size="$icon.16" />
</Flex>
</TouchableArea>
)}
</Flex>
<Flex row alignItems="flex-start" gap="$spacing16" pt="$spacing4">
<TouchableArea scaleTo={SCAN_ICON_ACTIVE_SCALE} activeOpacity={1} onPress={onPressScan}>
<ScanHome color="$neutral2" size="$icon.24" />
</TouchableArea>
<RotatingSettingsIcon onPressSettings={onPressSettings} />
</Flex>
</Flex>
</Flex>
)}
</Flex>
)
......
import type { Meta, StoryObj } from '@storybook/react'
import { CopyTextButton } from 'src/components/buttons/CopyTextButton'
import { StorybookTitles } from 'ui/src/storybook'
const meta = {
title: StorybookTitles.Atoms,
title: 'Components/Buttons',
component: CopyTextButton,
} satisfies Meta<typeof CopyTextButton>
......
......@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ListRenderItem, ListRenderItemInfo, StyleSheet } from 'react-native'
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 { FavoriteTokensGrid } from 'src/components/explore/FavoriteTokensGrid'
import { FavoriteWalletsGrid } from 'src/components/explore/FavoriteWalletsGrid'
......@@ -13,7 +13,7 @@ import { TokenItemData } from 'src/components/explore/TokenItemData'
import { AnimatedBottomSheetFlatList } from 'src/components/layout/AnimatedFlatList'
import { AutoScrollProps } from 'src/components/sortableGrid/types'
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 { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo'
......@@ -191,7 +191,7 @@ function NetworkPillsRow({
const renderItem: ListRenderItem<UniverseChainId> = useCallback(
({ item }: ListRenderItemInfo<UniverseChainId>) => {
return (
<AnimatedTouchableArea entering={FadeIn} exiting={FadeOut} onPress={() => onSelectNetwork(item)}>
<TouchableArea onPress={() => onSelectNetwork(item)}>
<NetworkPill
key={item}
showIcon
......@@ -207,7 +207,7 @@ function NetworkPillsRow({
showBackgroundColor={false}
textVariant="buttonLabel3"
/>
</AnimatedTouchableArea>
</TouchableArea>
)
},
[colors.neutral1.val, onSelectNetwork, selectedNetwork],
......
......@@ -176,6 +176,8 @@ export function SearchResultsSection({
return (
<Flex grow gap="$spacing8" pb="$spacing36">
<AnimatedBottomSheetFlashList
// when switching networks, we want to rerender the list to prevent any layout misalignments
key={selectedChain}
estimatedItemSize={ESTIMATED_ITEM_SIZE}
ListEmptyComponent={
<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 { StyleSheet } from 'react-native'
import Svg, { Circle } from 'react-native-svg'
import { BackButtonView } from 'src/components/layout/BackButtonView'
import { SeedPhraseDisplay } from 'src/components/mnemonic/SeedPhraseDisplay'
import { APP_STORE_LINK } from 'src/constants/urls'
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 { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
import { NewTag } from 'uniswap/src/components/pill/NewTag'
import { DynamicConfigs, ForceUpgradeConfigKey } from 'uniswap/src/features/gating/configs'
import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
......@@ -19,7 +22,7 @@ export function ForceUpgradeModal(): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
const forceUpgradeStatusString = useDynamicConfigValue(
DynamicConfigs.MobileForceUpgrade,
DynamicConfigs.ForceUpgrade,
ForceUpgradeConfigKey.Status,
'' as string,
)
......@@ -64,26 +67,83 @@ export function ForceUpgradeModal(): JSX.Element {
// the force upgrade screen on error, hence we fallback to the global error boundary
return (
<>
<WarningModal
acknowledgeText={t('forceUpgrade.action.confirm')}
<Modal
backgroundColor={colors.surface1.val}
hideHandlebar={upgradeStatus === UpgradeStatus.Required}
isDismissible={upgradeStatus !== UpgradeStatus.Required}
isOpen={isVisible}
modalName={ModalName.ForceUpgradeModal}
severity={WarningSeverity.High}
title={t('forceUpgrade.title')}
isModalOpen={isVisible}
name={ModalName.ForceUpgradeModal}
onClose={onClose}
onAcknowledge={onPressConfirm}
>
<Text color="$neutral2" textAlign="center" variant="body2">
<Flex
centered
gap="$spacing24"
pb={isWeb ? '$none' : '$spacing12'}
pt={upgradeStatus === UpgradeStatus.Required ? '$spacing24' : '$spacing12'}
px={isWeb ? '$none' : '$spacing24'}
>
<Flex
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 && (
<Text color="$accent1" variant="buttonLabel2" onPress={onPressViewRecovery}>
<Button size="medium" theme="secondary" width="100%" onPress={onPressViewRecovery}>
<Text color="$neutral1" variant="buttonLabel2">
{t('forceUpgrade.action.recoveryPhrase')}
</Text>
</Button>
)}
</WarningModal>
</Flex>
</Flex>
</Modal>
{mnemonicId && showSeedPhrase && (
<Modal fullScreen backgroundColor={colors.surface1.val} name={ModalName.ForceUpgradeModal} onClose={onDismiss}>
<Flex fill gap="$spacing16" px="$spacing24" py="$spacing24">
......@@ -103,3 +163,48 @@ export function ForceUpgradeModal(): JSX.Element {
}
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 { call } from 'redux-saga/effects'
import { navigationRef } from 'src/app/navigation/NavigationContainer'
import { navigationRef } from 'src/app/navigation/navigationRef'
import {
handleDeepLink,
handleUniswapAppDeepLink,
......
......@@ -15,6 +15,7 @@ import {
UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM,
UNISWAP_WALLETCONNECT_URL,
} from 'src/features/deepLinking/constants'
import { handleOffRampReturnLink } from 'src/features/deepLinking/handleOffRampReturnLinkSaga'
import { handleOnRampReturnLink } from 'src/features/deepLinking/handleOnRampReturnLinkSaga'
import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga'
import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga'
......@@ -207,6 +208,7 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) {
const screen = url.searchParams.get('screen')
const userAddress = url.searchParams.get('userAddress')
const fiatOnRamp = url.searchParams.get('fiatOnRamp') === 'true'
const fiatOffRamp = url.searchParams.get('fiatOffRamp') === 'true'
const activeAccount = yield* select(selectActiveAccount)
if (!activeAccount) {
......@@ -271,6 +273,8 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) {
case 'transaction':
if (fiatOnRamp) {
yield* call(handleOnRampReturnLink)
} else if (fiatOffRamp) {
yield* call(handleOffRampReturnLink, url)
} else {
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'
import { expectSaga } from 'redux-saga-test-plan'
import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga'
import { openModal } from 'src/features/modals/modalSlice'
import { DAI, UNI } from 'uniswap/src/constants/tokens'
import { AssetType } from 'uniswap/src/entities/assets'
import { DAI, UNI, USDC_UNICHAIN_SEPOLIA } from 'uniswap/src/constants/tokens'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
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'
const account = signerMnemonicAccount()
......@@ -29,44 +26,6 @@ const formSwapUrl = (
&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(
account.address,
UniverseChainId.Mainnet,
......@@ -76,6 +35,15 @@ const swapUrl = formSwapUrl(
'100',
)
const testnetSwapUrl = formSwapUrl(
account.address,
UniverseChainId.Sepolia,
USDC_UNICHAIN_SEPOLIA.address,
UNI[UniverseChainId.Sepolia].address,
'input',
'100',
)
const invalidOutputCurrencySwapUrl = formSwapUrl(
account.address,
UniverseChainId.Mainnet,
......@@ -121,19 +89,16 @@ const invalidCurrencyFieldSwapUrl = formSwapUrl(
'100',
)
const swapFormState = formTransactionState(
UniverseChainId.Mainnet,
DAI.address,
UNI[UniverseChainId.Mainnet].address,
'input',
'100',
) as TransactionState
describe(handleSwapLink, () => {
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)
.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()
})
})
......
......@@ -2,7 +2,8 @@ import { BigNumber } from 'ethers'
import { openModal } from 'src/features/modals/modalSlice'
import { put } from 'typed-redux-saga'
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 { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { CurrencyField } from 'uniswap/src/types/currency'
......@@ -10,6 +11,16 @@ import { getValidAddress } from 'uniswap/src/utils/addresses'
import { currencyIdToAddress, currencyIdToChain } from 'uniswap/src/utils/currencyId'
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) {
try {
const { inputChain, inputAddress, outputChain, outputAddress, exactCurrencyField, exactAmountToken } =
......@@ -34,7 +45,27 @@ export function* handleSwapLink(url: URL) {
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 }))
// 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) {
logger.error(error, { tags: { file: 'handleSwapLinkSaga', function: 'handleSwapLink' } })
yield* put(openModal({ name: ModalName.Swap }))
......@@ -77,11 +108,11 @@ const parseAndValidateSwapParams = (url: URL) => {
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')
}
if (!SUPPORTED_CHAIN_IDS.includes(outputChain)) {
if (!ALL_CHAIN_IDS.includes(outputChain)) {
throw new Error('Invalid outputCurrencyId. Chain ID is not supported')
}
......@@ -95,6 +126,13 @@ const parseAndValidateSwapParams = (url: URL) => {
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
return {
......
......@@ -2,10 +2,12 @@ import { ExploreModalState } from 'src/app/modals/ExploreModalState'
import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchModalState'
import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState'
import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
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 { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
......@@ -33,8 +35,9 @@ export interface ModalsState {
[ModalName.RemoveWallet]: AppModalState<RemoveWalletModalState>
[ModalName.RestoreWallet]: AppModalState<undefined>
[ModalName.Scantastic]: AppModalState<ScantasticModalState>
[ModalName.Send]: AppModalState<TransactionState>
[ModalName.Send]: AppModalState<TransactionState & { sendScreen: TransactionScreen }>
[ModalName.Swap]: AppModalState<TransactionState>
[ModalName.TestnetSwitchModal]: AppModalState<TestnetSwitchModalState>
[ModalName.UnitagsIntro]: AppModalState<{
address: Address
entryPoint: MobileScreens.Home | MobileScreens.Settings
......
......@@ -5,9 +5,11 @@ import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWallet
import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState'
import { ModalsState } from 'src/features/modals/ModalsState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchModalState'
import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState'
import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState'
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 { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { getKeys } from 'utilities/src/primitives/objects'
......@@ -60,6 +62,11 @@ type ScantasticModalParams = {
initialState: ScantasticModalState
}
type TestnetSwitchModalParams = {
name: typeof ModalName.TestnetSwitchModal
initialState?: TestnetSwitchModalState
}
type RemoveWalletModalParams = {
name: typeof ModalName.RemoveWallet
initialState?: RemoveWalletModalState
......@@ -74,7 +81,12 @@ type WalletConnectModalParams = {
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 = {
name: typeof ModalName.UnitagsIntro
......@@ -114,6 +126,7 @@ export type OpenModalParams =
| ReceiveCryptoModalParams
| LanguageSelectorModalParams
| ScantasticModalParams
| TestnetSwitchModalParams
| RemoveWalletModalParams
| SendModalParams
| 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({
const headerHeight = useHeaderHeight()
const insets = useAppInsets()
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'
......@@ -82,7 +84,7 @@ export function OnboardingScreen({
>
{/* Text content */}
<Flex centered gap="$spacing8" m="$spacing12" mb={ignoreTextContainerMarginBottom ? '$none' : undefined}>
{Icon && (
{showIcon && Icon && (
<Flex centered mb="$spacing4">
<Flex centered backgroundColor="$surface3" borderRadius="$rounded8" p="$spacing12">
<Icon color="$neutral1" size="$icon.18" />
......
......@@ -44,16 +44,20 @@ export function SendFlow(): JSX.Element {
onClose={onClose}
>
<SendContextProvider prefilledTransactionState={initialState}>
<CurrentScreen />
<CurrentScreen screenOverride={initialState?.sendScreen} />
</SendContextProvider>
</TransactionModal>
)
}
function CurrentScreen(): JSX.Element {
const { screen } = useTransactionModalContext()
function CurrentScreen({ screenOverride }: { screenOverride?: TransactionScreen }): JSX.Element {
const { screen, setScreen } = useTransactionModalContext()
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
// the modals are rendered correctly, and animations can properly measure the available space for the decimal pad.
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 = (
}
}
function isUtf8(str: string): boolean {
try {
const decoded = new TextDecoder('utf-8').decode(new TextEncoder().encode(str))
// if the encoded -> decoded string matches the original string (ie no chars swapped),
// then it's valid utf-8
return decoded === str
} catch {
return false
}
}
export function decodeMessage(value: string): string {
if (utils.isHexString(value) && isUtf8(value)) {
try {
if (utils.isHexString(value)) {
const decoded = utils.toUtf8String(value)
if (decoded?.trim()) {
return decoded
}
}
return value
} catch {
return value
}
}
/**
......
......@@ -53,6 +53,8 @@ export function ExploreScreen(): JSX.Element {
const [isSearchMode, setIsSearchMode] = useState<boolean>(false)
const textInputRef = useRef<TextInput>(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 => {
setSearchQuery(newSearchFilter)
......@@ -132,8 +134,56 @@ export function ExploreScreen(): JSX.Element {
)}
</KeyboardAvoidingView>
) : (
isSheetReady && <ExploreSections listRef={listRef} />
isSheetReady && canRenderList && <ExploreSections listRef={listRef} />
)}
</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 |
refundWalletAddress: activeAccountAddress,
externalCustomerId: activeAccountAddress,
externalSessionId: externalTransactionId,
redirectUrl: `${uniswapUrls.redirectUrlBase}?screen=transaction&fiatOffRamp=true&userAddress=${activeAccountAddress}`,
redirectUrl: `${uniswapUrls.redirectUrlBase}?screen=transaction&fiatOffRamp=true&userAddress=${activeAccountAddress}&externalTransactionId=${externalTransactionId}`,
}
: skipToken,
)
......
......@@ -366,6 +366,14 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
})
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)
if (isSupportedFORCurrency(currency)) {
setQuoteCurrency(currency)
......@@ -427,14 +435,18 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
}
}, [navigateToSwapFlow, unsupportedCurrency])
const onPillToggle = (option: string | number): void => {
setIsOffRamp(option === RampToggle.SELL)
const resetAmount = useCallback(() => {
setValue('')
setFiatAmount(0)
setTokenAmount(0)
valueRef.current = ''
resetSelection({ start: 0 })
setSelectedQuote(undefined)
}, [setValue, setFiatAmount, setTokenAmount, valueRef, resetSelection, setSelectedQuote])
const onPillToggle = (option: string | number): void => {
setIsOffRamp(option === RampToggle.SELL)
resetAmount()
setQuoteCurrency(defaultCurrency)
sendAnalyticsEvent(FiatOffRampEventName.FORBuySellToggled, {
......
......@@ -15,7 +15,6 @@ import Animated, {
useDerivedValue,
useSharedValue,
} from 'react-native-reanimated'
import { SvgProps } from 'react-native-svg'
import { SceneRendererProps, TabBar } from 'react-native-tab-view'
import { useDispatch, useSelector } from 'react-redux'
import { NavBar, SWAP_BUTTON_HEIGHT } from 'src/app/navigation/NavBar'
......@@ -45,16 +44,12 @@ import { openModal } from 'src/features/modals/modalSlice'
import { selectSomeModalOpen } from 'src/features/modals/selectSomeModalOpen'
import { AIAssistantOverlay } from 'src/features/openai/AIAssistantOverlay'
import { useWalletRestore } from 'src/features/wallet/hooks'
import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { hideSplashScreen } from 'src/utils/splashScreen'
import { useOpenBackupReminderModal } from 'src/utils/useOpenBackupReminderModal'
import { Flex, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src'
import ReceiveIcon from 'ui/src/assets/icons/arrow-down-circle.svg'
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 { Flex, GeneratedIcon, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src'
import { ArrowDownCircle, Buy, SendAction } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { iconSizes, spacing } from 'ui/src/theme'
......@@ -376,12 +371,6 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
await hapticFeedback.light()
}, [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 () => {
dispatch(openModal({ name: ModalName.Send }))
await triggerHaptics()
......@@ -400,11 +389,6 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
// Hide actions when active account isn't a signer account.
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)
......@@ -425,41 +409,34 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
)
}, [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(
(): QuickAction[] => [
{
Icon: BuyIcon,
Icon: Buy,
eventName: MobileEventName.FiatOnRampQuickActionButtonPressed,
iconScale: 1.2,
label: buyLabel,
name: ElementName.Buy,
sentryLabel: 'BuyActionButton',
onPress: onPressBuy,
},
{
Icon: SendIcon,
iconScale: 1.1,
Icon: SendAction,
label: sendLabel,
name: ElementName.Send,
sentryLabel: 'SendActionButton',
onPress: onPressSend,
},
{
Icon: ReceiveIcon,
Icon: ArrowDownCircle,
label: receiveLabel,
name: ElementName.Receive,
sentryLabel: 'ReceiveActionButton',
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
......@@ -479,12 +456,7 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
const contentHeader = useMemo(() => {
return (
<Flex
backgroundColor="$surface1"
gap="$spacing8"
pb={showEmptyWalletState ? '$spacing8' : '$spacing16'}
px="$spacing12"
>
<Flex backgroundColor="$surface1" pb={showEmptyWalletState ? '$spacing8' : '$spacing16'} px="$spacing12">
<TestnetModeModal
unsupported
isOpen={isTestnetWarningModalOpen}
......@@ -492,15 +464,15 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
onClose={handleTestnetWarningModalClose}
/>
<AccountHeader />
<Flex pb="$spacing8" px="$spacing12">
<Flex py="$spacing20" px="$spacing12">
<PortfolioBalance owner={activeAccount.address} />
</Flex>
{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">
<Text allowFontScaling={false} color="$neutral2" variant="body2">
<Text color="$neutral2" variant="body2">
{viewOnlyLabel}
</Text>
</Flex>
......@@ -797,69 +769,51 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
}
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
iconScale?: number
/* Label to display for the action */
label: string
/* Name of the element to log when the action is triggered */
name: ElementNameType
sentryLabel: string
/* Callback to execute when the action is triggered */
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 {
return (
<Flex centered row gap="$spacing12" px="$spacing12">
{actions.map((action) => (
<ActionButton
key={action.name}
Icon={action.Icon}
eventName={action.eventName}
flex={1}
iconScale={action.iconScale}
label={action.label}
name={action.name}
sentry-label={action.sentryLabel}
onPress={action.onPress}
/>
))}
</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
const iconSize = iconSizes.icon24
const contentColor = colors.accent1.val
const activeScale = 0.96
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>
<Flex centered row gap="$spacing12" px="$spacing12">
{actions.map(({ eventName, name, label, Icon, onPress }) => (
<Trace key={name} logPress element={name} eventOnTrigger={eventName}>
<TouchableArea flex={1} scaleTo={activeScale} onPress={onPress}>
<Flex
fill
backgroundColor="$accent2"
borderRadius="$rounded20"
pt="$spacing16"
pb="$spacing12"
px="$spacing12"
gap="$spacing4"
justifyContent="space-between"
>
<Icon color={contentColor} size={iconSize} strokeWidth={2} />
<Text color={contentColor} variant="buttonLabel2">
{label}
</Text>
</Flex>
</TouchableArea>
</Trace>
))}
</Flex>
)
}
......@@ -29,6 +29,7 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName'
import { logger } from 'utilities/src/logger/logger'
import { useTimeout } from 'utilities/src/time/timing'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
......@@ -249,7 +250,9 @@ export function OnDeviceRecoveryScreen({
</Flex>
</Flex>
<WarningModal
caption={t('onboarding.import.onDeviceRecovery.warning.caption')}
caption={t('onboarding.import.onDeviceRecovery.warning.caption', {
cloudProvider: getCloudProviderName(),
})}
rejectText={t('common.button.back')}
acknowledgeText={t('common.button.continue')}
icon={<PapersText color={colors.neutral1.get()} size="$icon.20" strokeWidth={1.5} />}
......
......@@ -111,6 +111,7 @@ export function SettingsScreen(): JSX.Element {
const fireAnalytic = (): void =>
sendAnalyticsEvent(WalletEventName.TestnetModeToggled, {
enabled: newIsTestnetMode,
location: 'settings',
})
setTimeout(() => {
......
import { useFocusEffect, useNavigation } from '@react-navigation/core'
import { useNavigation } from '@react-navigation/core'
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 { ListRenderItemInfo, SectionList } from 'react-native'
import { SvgProps } from 'react-native-svg'
......@@ -20,12 +20,7 @@ import {
import { BackHeader } from 'src/components/layout/BackHeader'
import { Screen } from 'src/components/layout/Screen'
import { openModal } from 'src/features/modals/modalSlice'
import { promptPushPermission } from 'src/features/notifications/Onesignal'
import {
NotificationPermission,
useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen'
import { useNotificationToggle } from 'src/features/notifications/hooks/useNotificationsToggle'
import { Button, Flex, Switch, Text, useSporeColors } from 'ui/src'
import NotificationIcon from 'ui/src/assets/icons/bell.svg'
import GlobalIcon from 'ui/src/assets/icons/global.svg'
......@@ -40,11 +35,14 @@ import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useAccounts, useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks'
import { useAccounts } from 'wallet/src/features/wallet/hooks'
type Props = NativeStackScreenProps<SettingsStackParamList, MobileScreens.SettingsWallet>
const onPermissionChanged = (enabled: boolean): void => {
sendAnalyticsEvent(MobileEventName.NotificationsToggled, { enabled })
}
// Specific design request not in standard sizing type
const UNICON_ICON_SIZE = 56
......@@ -64,10 +62,6 @@ export function SettingsWallet({
const readonly = currentAccount?.type === AccountType.Readonly
const navigation = useNavigation<SettingsStackNavigationProp & OnboardingStackNavigationProp>()
const notificationOSPermission = useNotificationOSPermissionsEnabled()
const notificationsEnabledOnFirebase = useSelectAccountNotificationSetting(address)
const [notificationSwitchEnabled, setNotificationSwitchEnabled] = useState<boolean>(notificationsEnabledOnFirebase)
const showEditProfile = !readonly
useEffect(() => {
......@@ -77,47 +71,6 @@ export function SettingsWallet({
}
}, [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 = {
color: colors.neutral2.get(),
height: iconSizes.icon24,
......@@ -141,14 +94,7 @@ export function SettingsWallet({
data: [
...(showEditProfile ? [] : [editNicknameSectionOption]),
{
action: (
<Switch
checked={notificationSwitchEnabled}
disabled={notificationOSPermission === NotificationPermission.Loading}
variant="branded"
onCheckedChange={onChangeNotificationSettings}
/>
),
action: <NotificationsSwitch address={address} />,
text: t('settings.setting.wallet.notifications.title'),
icon: <NotificationIcon {...iconProps} />,
},
......@@ -267,3 +213,13 @@ function AddressDisplayHeader({ address }: { address: Address }): JSX.Element {
</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 {
import React, { PropsWithChildren } from 'react'
import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider'
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 { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
......
......@@ -13,6 +13,7 @@ ignores: [
'process',
'madge',
# Dependencies that depcheck thinks are missing but are actually present or never used
'stories',
## package.json scripts
'esbuild-register',
## GraphQL
......@@ -30,6 +31,17 @@ ignores: [
'@babel/preset-env',
'eslint-plugin-import',
'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
'@types/testing-library__cypress',
## i18n
......
......@@ -15,6 +15,7 @@ REACT_APP_QUICKNODE_BASE_RPC_URL=""
REACT_APP_QUICKNODE_BLAST_RPC_URL=""
REACT_APP_QUICKNODE_BNB_RPC_URL=""
REACT_APP_QUICKNODE_CELO_RPC_URL=""
REACT_APP_QUICKNODE_MONAD_TESTNET_RPC_URL=""
REACT_APP_QUICKNODE_OP_RPC_URL=""
REACT_APP_QUICKNODE_POLYGON_RPC_URL=""
REACT_APP_MOONPAY_API="https://api.moonpay.com"
......
......@@ -3,3 +3,4 @@ babel.config.js
jest.config.js
metro.config.js
node_modules
.storybook/stories
......@@ -8,7 +8,7 @@ rulesDirPlugin.RULES_DIR = 'eslint_rules'
module.exports = {
root: true,
extends: ['@uniswap/eslint-config/react'],
extends: ['@uniswap/eslint-config/react', 'plugin:storybook/recommended'],
plugins: ['rulesdir'],
rules: {
......
......@@ -54,3 +54,5 @@ cypress/screenshots
.vercel
.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')
const { readFileSync } = require('fs')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
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 { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
......
......@@ -30,7 +30,9 @@
"test:cloud": "yarn jest functions --config=functions/jest.config.json",
"cypress:open": "cypress open --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": {
"hooks": {
......@@ -70,10 +72,19 @@
},
"devDependencies": {
"@babel/preset-env": "7.23.3",
"@chromatic-com/storybook": "3.2.2",
"@cloudflare/workers-types": "4.20231025.0",
"@craco/craco": "7.1.0",
"@crowdin/cli": "3.14.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/jest": "0.2.29",
"@swc/plugin-styled-components": "1.5.97",
......@@ -114,12 +125,14 @@
"concurrently": "8.2.2",
"cypress": "12.17.4",
"cypress-hardhat": "2.5.3",
"depcheck": "1.4.7",
"dotenv": "16.0.3",
"dotenv-cli": "7.1.0",
"esbuild-register": "3.6.0",
"eslint": "8.44.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-rulesdir": "0.2.2",
"eslint-plugin-storybook": "0.8.0",
"hardhat": "2.22.16",
"husky": "8.0.3",
"jest": "29.7.0",
......@@ -133,11 +146,13 @@
"path-browserify": "1.0.1",
"postinstall-postinstall": "2.1.0",
"process": "0.11.10",
"prop-types": "15.8.1",
"react-scripts": "5.0.1",
"resize-observer-polyfill": "1.5.1",
"serve": "14.2.4",
"source-map-explorer": "2.5.3",
"start-server-and-test": "2.0.0",
"storybook": "8.4.2",
"swc-loader": "0.2.6",
"terser": "5.24.0",
"terser-webpack-plugin": "5.3.9",
......@@ -172,6 +187,7 @@
"@sentry/core": "7.80.0",
"@sentry/react": "7.80.0",
"@sentry/types": "7.80.0",
"@svgr/webpack": "8.0.1",
"@tamagui/core": "1.114.4",
"@tamagui/portal": "1.114.4",
"@tamagui/react-native-svg": "1.114.4",
......@@ -189,7 +205,7 @@
"@uniswap/permit2-sdk": "1.3.0",
"@uniswap/redux-multicall": "1.1.8",
"@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/token-lists": "1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18",
......@@ -269,6 +285,7 @@
"styled-components": "5.3.11",
"tamagui": "1.114.4",
"tiny-invariant": "1.3.1",
"ts-loader": "9.5.1",
"typed-redux-saga": "1.5.0",
"ui": "workspace:^",
"uniswap": "workspace:^",
......
......@@ -7215,4 +7215,134 @@
<lastmod>2024-12-06T22:01:49.956Z</lastmod>
<priority>0.8</priority>
</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>
\ No newline at end of file
......@@ -8385,4 +8385,249 @@
<lastmod>2024-12-07T02:42:16.799Z</lastmod>
<priority>0.8</priority>
</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>
\ No newline at end of file
......@@ -7,42 +7,6 @@ exports[`CancelOrdersDialog should render limit order text 1`] = `
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 {
color: #222222;
-webkit-letter-spacing: -0.01em;
......@@ -226,6 +190,42 @@ exports[`CancelOrdersDialog should render limit order text 1`] = `
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 {
width: -webkit-fit-content;
width: -moz-fit-content;
......@@ -626,42 +626,6 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = `
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 {
color: #222222;
-webkit-letter-spacing: -0.01em;
......@@ -845,6 +809,42 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = `
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 {
width: -webkit-fit-content;
width: -moz-fit-content;
......
......@@ -7,42 +7,6 @@ exports[`OrderContent should render without error, filled order 1`] = `
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 {
color: #222222;
-webkit-letter-spacing: -0.01em;
......@@ -93,6 +57,42 @@ exports[`OrderContent should render without error, filled order 1`] = `
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 {
display: -webkit-box;
display: -webkit-flex;
......@@ -463,42 +463,6 @@ exports[`OrderContent should render without error, limit order 1`] = `
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 {
color: #222222;
-webkit-letter-spacing: -0.01em;
......@@ -621,6 +585,42 @@ exports[`OrderContent should render without error, limit order 1`] = `
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 {
display: -webkit-box;
display: -webkit-flex;
......@@ -1035,42 +1035,6 @@ exports[`OrderContent should render without error, open order 1`] = `
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 {
color: #222222;
-webkit-letter-spacing: -0.01em;
......@@ -1174,6 +1138,42 @@ exports[`OrderContent should render without error, open order 1`] = `
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 {
display: -webkit-box;
display: -webkit-flex;
......
......@@ -125,6 +125,11 @@ function callsV4PositionManagerContract(assetActivity: TransactionActivity) {
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)
}
function callsPositionManagerContract(assetActivity: TransactionActivity) {
......
......@@ -7,6 +7,29 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = `
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 {
width: 100%;
display: -webkit-box;
......@@ -42,29 +65,6 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = `
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 {
display: -webkit-box;
display: -webkit-flex;
......
......@@ -39,6 +39,95 @@ exports[`AccountDrawer tests AccountDrawer default styles 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 {
width: 100%;
display: -webkit-box;
......@@ -158,95 +247,6 @@ exports[`AccountDrawer tests AccountDrawer default styles 1`] = `
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 {
display: -webkit-box;
display: -webkit-flex;
......
import { RowBetween } from 'components/deprecated/Row'
import styled, { DefaultTheme } from 'lib/styled-components'
import { darken } from 'polished'
import { forwardRef } from 'react'
import { ChevronDown } from 'react-feather'
import { ButtonProps as ButtonPropsOriginal, Button as RebassButton } from 'rebass/styled-components'
export { default as LoadingButtonSpinner } from './LoadingButtonSpinner'
......@@ -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 {
small,
medium,
......
import { Currency } from '@uniswap/sdk-core'
import { Currency, Percent } from '@uniswap/sdk-core'
import { AxisRight } from 'components/Charts/ActiveLiquidityChart/AxisRight'
import { Brush2 } from 'components/Charts/ActiveLiquidityChart/Brush2'
import { HorizontalArea } from 'components/Charts/ActiveLiquidityChart/HorizontalArea'
......@@ -7,7 +7,9 @@ import { TickTooltip } from 'components/Charts/ActiveLiquidityChart/TickTooltip'
import { ChartEntry } from 'components/LiquidityChartRangeInput/types'
import { max as getMax, scaleLinear } from 'd3'
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 yAccessor = (d: ChartEntry) => d.price0
......@@ -53,6 +55,11 @@ function findClosestElementBinarySearch(data: ChartEntry[], target?: number) {
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
* x-y coordinate plane to show the data, but with the axes flipped so lower
......@@ -69,6 +76,8 @@ export function ActiveLiquidityChart2({
brushDomain,
onBrushDomainChange,
disableBrushInteraction,
showDiffIndicators,
isMobile,
}: {
id?: string
currency0: Currency
......@@ -80,10 +89,13 @@ export function ActiveLiquidityChart2({
max?: number
}
disableBrushInteraction?: boolean
showDiffIndicators?: boolean
dimensions: { width: number; height: number; contentWidth: number; axisLabelPaneWidth: number }
brushDomain?: [number, number]
onBrushDomainChange: (domain: [number, number], mode: string | undefined) => void
isMobile?: boolean
}) {
const { formatPercent } = useFormatter()
const colors = useSporeColors()
const svgRef = useRef<SVGSVGElement | null>(null)
const [hoverY, setHoverY] = useState<number>()
......@@ -124,6 +136,9 @@ export function ActiveLiquidityChart2({
}
}, [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 (
<>
{hoverY && hoveredTick && (
......@@ -138,6 +153,42 @@ export function ActiveLiquidityChart2({
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
ref={svgRef}
width="100%"
......@@ -186,8 +237,8 @@ export function ActiveLiquidityChart2({
xValue={xAccessor}
yValue={yAccessor}
brushDomain={brushDomain}
fill={brushDomain ? colors.neutral1.val : colors.accent1.val}
selectedFill={colors.accent1.val}
fill={opacify(isMobile ? 10 : 100, brushDomain ? colors.neutral1.val : colors.accent1.val)}
selectedFill={opacify(isMobile ? 10 : 100, colors.accent1.val)}
containerHeight={height}
containerWidth={width - axisLabelPaneWidth}
/>
......@@ -209,6 +260,7 @@ export function ActiveLiquidityChart2({
/>
)}
{isMobile ? null : (
<AxisRight
yScale={yScale}
offset={width - contentWidth}
......@@ -217,6 +269,7 @@ export function ActiveLiquidityChart2({
max={brushDomain?.[1]}
height={height}
/>
)}
</g>
<Brush2
......
import { NumberValue, ScaleLinear, axisRight, Axis as d3Axis, select } from 'd3'
import styled from 'lib/styled-components'
import { useMemo } from 'react'
import { NumberType, useFormatter } from 'utils/formatNumbers'
const StyledGroup = styled.g`
line {
......@@ -12,7 +13,7 @@ const StyledGroup = styled.g`
}
`
const TEXT_Y_OFFSET = 10
const TEXT_Y_OFFSET = 5
const Axis = ({
axisGenerator,
......@@ -61,6 +62,7 @@ export const AxisRight = ({
current?: number
max?: number
}) => {
const { formatNumber } = useFormatter()
const tickValues = useMemo(() => {
const minCoordinate = min ? yScale(min) : undefined
const maxCoordinate = max ? yScale(max) : undefined
......@@ -76,7 +78,18 @@ export const AxisRight = ({
return (
<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>
)
}
......@@ -68,13 +68,17 @@ export const Brush2 = ({
// keep local and external brush extent in sync
// i.e. snap to ticks on brush end
const [brushInProgress, setBrushInProgress] = useState(false)
useEffect(() => {
if (brushInProgress) {
return
}
setLocalBrushExtent(brushExtent)
}, [brushExtent])
}, [brushExtent, brushInProgress])
// initialize the brush
useEffect(() => {
if (!brushRef.current) {
if (!brushRef.current || brushInProgress) {
return
}
......@@ -90,8 +94,14 @@ export const Brush2 = ({
])
.handleSize(30)
.filter(() => interactive)
.filter((event) => {
// Allow interactions only if the event target is part of the brush selection or handles
const target = event.target as SVGElement
return target.classList.contains('selection') || target.classList.contains('handle')
})
.on('brush', (event: D3BrushEvent<unknown>) => {
const { selection } = event
setBrushInProgress(true)
if (!selection) {
setLocalBrushExtent(null)
......@@ -116,6 +126,7 @@ export const Brush2 = ({
setBrushExtent(priceExtent, mode)
}
setLocalBrushExtent(priceExtent)
setBrushInProgress(false)
})
brushBehavior.current(select(brushRef.current))
......@@ -126,6 +137,8 @@ export const Brush2 = ({
.call(brushBehavior.current.move as any, scaledExtent)
}
select(brushRef.current).selectAll('.overlay').attr('cursor', 'default')
// brush linear gradient
select(brushRef.current)
.selectAll('.selection')
......@@ -133,7 +146,7 @@ export const Brush2 = ({
.attr('fill-opacity', '0.1')
.attr('fill', `url(#${id}-gradient-selection)`)
.attr('cursor', 'grab')
}, [brushExtent, id, height, interactive, previousBrushExtent, yScale, width, setBrushExtent])
}, [brushExtent, id, height, interactive, previousBrushExtent, yScale, width, setBrushExtent, brushInProgress])
// respond to yScale changes only
useEffect(() => {
......
......@@ -3,7 +3,6 @@ import { ScaleLinear } from 'd3'
import styled from 'lib/styled-components'
const Bar = styled.rect<{ fill?: string }>`
opacity: 0.5;
stroke: ${({ fill, theme }) => fill ?? theme.accent1};
fill: ${({ fill, theme }) => fill ?? theme.accent1};
`
......
......@@ -12,7 +12,6 @@ import {
} from 'components/Charts/ChartModel'
import { PriceChartData } from 'components/Charts/PriceChart'
import { PriceChartType, formatTickMarks } from 'components/Charts/utils'
import { MissingDataIcon } from 'components/Table/icons'
import { DataQuality } from 'components/Tokens/TokenDetails/ChartSection/util'
import { usePoolPriceChartData } from 'hooks/usePoolPriceChartData'
import { useTheme } from 'lib/styled-components'
......@@ -25,12 +24,11 @@ import {
} from 'pages/Pool/Positions/create/utils'
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
import { opacify } from 'theme/utils'
import { Flex, FlexProps, Shine, TamaguiElement, Text, assertWebElement } from 'ui/src'
import { Flex, FlexProps, Shine, TamaguiElement, assertWebElement } from 'ui/src'
import { LoadingPriceCurve } from 'ui/src/components/icons/LoadingPriceCurve'
import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { useTranslation } from 'uniswap/src/i18n'
const CHART_HEIGHT = 52
export const CHART_WIDTH = 224
......@@ -115,7 +113,7 @@ export class LPPriceChartModel extends ChartModel<PriceChartData> {
Math.pow(10, params.positionPriceLower.baseCurrency.decimals),
),
)
?.toSignificant(params.positionPriceLower.baseCurrency.decimals) ?? 0,
?.toSignificant(params.positionPriceLower.baseCurrency.decimals || 6) ?? 0,
)
this.positionRangeMax =
typeof params.positionPriceUpper === 'number'
......@@ -128,7 +126,7 @@ export class LPPriceChartModel extends ChartModel<PriceChartData> {
Math.pow(10, params.positionPriceUpper.baseCurrency.decimals),
),
)
?.toSignificant(params.positionPriceUpper.baseCurrency.decimals) ?? 0,
?.toSignificant(params.positionPriceUpper.baseCurrency.decimals || 6) ?? 0,
)
if (isEffectivelyInfinity(this.positionRangeMin)) {
......@@ -392,7 +390,6 @@ export function LiquidityPositionRangeChart({
grow = false,
}: LiquidityPositionRangeChartProps) {
const theme = useTheme()
const { t } = useTranslation()
const isV2 = version === ProtocolVersion.V2
const isV3 = version === ProtocolVersion.V3
const isV4 = version === ProtocolVersion.V4
......@@ -477,14 +474,7 @@ export function LiquidityPositionRangeChart({
overflow="hidden"
>
{priceData.loading && <LiquidityPositionRangeChartLoader size={chartWidth} />}
{dataUnavailable && (
<Flex row alignItems="center" gap="$gap12">
<MissingDataIcon height={36} width={36} />
<Text variant="body3" color="$neutral2">
{t('common.dataUnavailable')}
</Text>
</Flex>
)}
{dataUnavailable && <LoadingPriceCurve size={chartWidth} color="$neutral2" />}
{shouldRenderChart && (
<Flex width={grow ? chartWidth : width} $md={{ width: grow ? chartWidth : '100%' }}>
<Chart Model={LPPriceChartModel} params={chartParams} height={CHART_HEIGHT} />
......
import { isMobileWeb } from 'utilities/src/platform'
const RIGHT_AXIS_WIDTH = 64
const CHART_CONTAINER_WIDTH = 452 + RIGHT_AXIS_WIDTH
const LIQUIDITY_CHART_WIDTH = 68
const INTER_CHART_PADDING = 12
const CHART_HEIGHT = 164
const BOTTOM_AXIS_HEIGHT = 28
const loadedPriceChartWidth = CHART_CONTAINER_WIDTH - LIQUIDITY_CHART_WIDTH - INTER_CHART_PADDING - RIGHT_AXIS_WIDTH
const desktopSizes = {
rightAxisWidth: RIGHT_AXIS_WIDTH,
chartContainerWidth: CHART_CONTAINER_WIDTH,
liquidityChartWidth: LIQUIDITY_CHART_WIDTH,
interChartPadding: INTER_CHART_PADDING,
chartHeight: CHART_HEIGHT,
bottomAxisHeight: BOTTOM_AXIS_HEIGHT,
loadedPriceChartWidth,
}
const mobileSizes = {
rightAxisWidth: 0,
chartContainerWidth: 290,
liquidityChartWidth: 48,
interChartPadding: 0,
chartHeight: CHART_HEIGHT,
bottomAxisHeight: BOTTOM_AXIS_HEIGHT,
loadedPriceChartWidth: 290,
}
export function useRangeInputSizes(parentWidth?: number) {
return isMobileWeb
? {
...mobileSizes,
chartContainerWidth: parentWidth ?? mobileSizes.chartContainerWidth,
loadedPriceChartWidth: parentWidth ?? mobileSizes.loadedPriceChartWidth,
}
: desktopSizes
}
......@@ -7,42 +7,6 @@ exports[`<Dialog /> renders different button types 1`] = `
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;
}
.c16 {
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 {
color: #222222;
-webkit-letter-spacing: -0.01em;
......@@ -226,6 +190,42 @@ exports[`<Dialog /> renders different button types 1`] = `
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;
}
.c16 {
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 {
width: -webkit-fit-content;
width: -moz-fit-content;
......@@ -568,42 +568,6 @@ exports[`<Dialog /> renders the Dialog component correctly 1`] = `
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;
}
.c16 {
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 {
color: #222222;
-webkit-letter-spacing: -0.01em;
......@@ -729,6 +693,42 @@ exports[`<Dialog /> renders the Dialog component correctly 1`] = `
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;
}
.c16 {
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 {
width: -webkit-fit-content;
width: -moz-fit-content;
......
......@@ -253,6 +253,7 @@ export default function FeatureFlagModal() {
<FeatureFlagGroup name="New Chains">
<FeatureFlagOption flag={FeatureFlags.Zora} label="Enable Zora" />
<FeatureFlagOption flag={FeatureFlags.UnichainPromo} label="Unichain In App Promotion" />
<FeatureFlagOption flag={FeatureFlags.MonadTestnet} label="Enable Monad Testnet" />
</FeatureFlagGroup>
<FeatureFlagOption flag={FeatureFlags.L2NFTs} label="L2 NFTs" />
<FeatureFlagGroup name="Quick routes">
......
export const MOONPAY_SUPPORTED_CURRENCY_CODES = [
'eth',
'eth_arbitrum',
'eth_optimism',
'eth_polygon',
'eth_base',
'weth',
'wbtc',
'matic_polygon',
'polygon',
'usdc_arbitrum',
'usdc_optimism',
'usdc_polygon',
'usdc_base',
'usdc',
'usdt',
] as const
export type MoonpaySupportedCurrencyCode = (typeof MOONPAY_SUPPORTED_CURRENCY_CODES)[number]
This diff is collapsed.
import { WETH9 } from '@uniswap/sdk-core'
import { getDefaultCurrencyCode, parsePathParts } from 'components/FiatOnrampModal/utils'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import {
MATIC_MAINNET,
USDC_ARBITRUM,
USDC_MAINNET,
USDC_OPTIMISM,
USDC_POLYGON,
USDT,
WBTC,
WETH_POLYGON,
} from 'uniswap/src/constants/tokens'
import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
describe('getDefaultCurrencyCode', () => {
it('NATIVE/arbitrum should return the correct currency code', () => {
expect(getDefaultCurrencyCode(NATIVE_CHAIN_ID, Chain.Arbitrum)).toBe('eth_arbitrum')
})
it('NATIVE/optimism should return the correct currency code', () => {
expect(getDefaultCurrencyCode(NATIVE_CHAIN_ID, Chain.Optimism)).toBe('eth_optimism')
})
it('WETH/polygon should return the correct currency code', () => {
expect(getDefaultCurrencyCode(WETH_POLYGON.address, Chain.Polygon)).toBe('eth_polygon')
})
it('WETH/ethereum should return the correct currency code', () => {
expect(getDefaultCurrencyCode(WETH9[UniverseChainId.Mainnet].address, Chain.Ethereum)).toBe('weth')
})
it('WBTC/ethereum should return the correct currency code', () => {
expect(getDefaultCurrencyCode(WBTC.address, Chain.Ethereum)).toBe('wbtc')
})
it('NATIVE/polygon should return the correct currency code', () => {
expect(getDefaultCurrencyCode(NATIVE_CHAIN_ID, Chain.Polygon)).toBe('matic_polygon')
})
it('MATIC/ethereum should return the correct currency code', () => {
expect(getDefaultCurrencyCode(MATIC_MAINNET.address, Chain.Ethereum)).toBe('polygon')
})
it('USDC/arbitrum should return the correct currency code', () => {
expect(getDefaultCurrencyCode(USDC_ARBITRUM.address, Chain.Arbitrum)).toBe('usdc_arbitrum')
})
it('USDC/optimism should return the correct currency code', () => {
expect(getDefaultCurrencyCode(USDC_OPTIMISM.address, Chain.Optimism)).toBe('usdc_optimism')
})
it('USDC/polygon should return the correct currency code', () => {
expect(getDefaultCurrencyCode(USDC_POLYGON.address, Chain.Polygon)).toBe('usdc_polygon')
})
it('native/ethereum should return the correct currency code', () => {
expect(getDefaultCurrencyCode(NATIVE_CHAIN_ID, Chain.Ethereum)).toBe('eth')
})
it('usdc/ethereum should return the correct currency code', () => {
expect(getDefaultCurrencyCode(USDC_MAINNET.address, Chain.Ethereum)).toBe('usdc')
})
it('usdt/ethereum should return the correct currency code', () => {
expect(getDefaultCurrencyCode(USDT.address, Chain.Ethereum)).toBe('usdt')
})
it('chain/token mismatch should default to eth', () => {
expect(getDefaultCurrencyCode(USDC_ARBITRUM.address, Chain.Ethereum)).toBe('eth')
expect(getDefaultCurrencyCode(USDC_OPTIMISM.address, Chain.Ethereum)).toBe('eth')
expect(getDefaultCurrencyCode(USDC_POLYGON.address, Chain.Ethereum)).toBe('eth')
expect(getDefaultCurrencyCode(MATIC_MAINNET.address, Chain.Arbitrum)).toBe('eth')
})
})
describe('parseLocation', () => {
it('should parse the URL correctly', () => {
expect(parsePathParts('/tokens/ethereum/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')).toEqual({
chainId: UniverseChainId.Mainnet,
tokenAddress: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599',
})
expect(parsePathParts('tokens/ethereum/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')).toEqual({
chainId: UniverseChainId.Mainnet,
tokenAddress: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599',
})
expect(parsePathParts('/swap')).toEqual({
chainId: undefined,
tokenAddress: undefined,
})
})
})
This diff is collapsed.
......@@ -2,13 +2,14 @@ import { LiquidityModalHeader } from 'components/Liquidity/LiquidityModalHeader'
import { WebUniswapProvider } from 'components/Web3Provider/WebUniswapContext'
import { act, fireEvent, render } from 'test-utils/render'
import { TransactionSettingsContextProvider } from 'uniswap/src/features/transactions/settings/contexts/TransactionSettingsContext'
import { TransactionSettingKey } from 'uniswap/src/features/transactions/settings/slice'
describe('LiquidityModalHeader', () => {
it('should render with given title and call close callback', () => {
const onClose = jest.fn()
const { getByText, getByTestId } = render(
<WebUniswapProvider>
<TransactionSettingsContextProvider>
<TransactionSettingsContextProvider settingKey={TransactionSettingKey.Swap}>
<LiquidityModalHeader title="Test Title" closeModal={onClose} />
</TransactionSettingsContextProvider>
,
......
......@@ -8,6 +8,7 @@ import {
BNB_LOGO,
CELO_LOGO,
ETHEREUM_LOGO,
MONAD_LOGO,
OPTIMISM_LOGO,
POLYGON_LOGO,
UNICHAIN_SEPOLIA_LOGO,
......@@ -133,6 +134,12 @@ export function getChainUI(chainId: UniverseChainId, darkMode: boolean): ChainUI
bgColor: '#fc0fa4',
textColor: '#fc0fa4',
}
case UniverseChainId.MonadTestnet:
return {
symbol: MONAD_LOGO,
bgColor: '#200052',
textColor: '#836EF9',
}
default:
return undefined
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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