+ How to use:
+ 1. Serve this directory: python3 -m http.server 8080
+ 2. Load the extension in Chrome (webpack dev build via bun start:webpack)
+ 3. Open http://localhost:8080 in Chrome
+ 4. All three cards should show their expected results (green = pass, red = fail)
+
+ Note: This test detects the Uniswap provider specifically via EIP-6963 (rdns: org.uniswap.app),
+ so other wallet extensions (MetaMask, etc.) won't cause false positives.
+
+
+
+
+
diff --git a/apps/extension/e2e/tests/smoke/basic-setup.test.ts b/apps/extension/e2e/tests/smoke/basic-setup.test.ts
index 8fee71a28df..515bd0e030f 100644
--- a/apps/extension/e2e/tests/smoke/basic-setup.test.ts
+++ b/apps/extension/e2e/tests/smoke/basic-setup.test.ts
@@ -30,15 +30,10 @@ test.describe('Basic Extension Setup', () => {
})
test('background script loads', async ({ context }) => {
- // Wait for background script/service worker to load
+ // Wait for service worker to load (MV3 extensions use service workers)
await sleep(ONE_SECOND_MS * 2)
- // Check for background pages or service workers
- const backgroundPages = context.backgroundPages()
const serviceWorkers = context.serviceWorkers()
-
- // Either background pages or service workers should exist
- const hasBackground = backgroundPages.length > 0 || serviceWorkers.length > 0
- expect(hasBackground).toBeTruthy()
+ expect(serviceWorkers.length).toBeGreaterThan(0)
})
})
diff --git a/apps/extension/e2e/tests/smoke/onboarding-flow.test.ts b/apps/extension/e2e/tests/smoke/onboarding-flow.test.ts
index 2a9d98a43cd..91af8cdf089 100644
--- a/apps/extension/e2e/tests/smoke/onboarding-flow.test.ts
+++ b/apps/extension/e2e/tests/smoke/onboarding-flow.test.ts
@@ -39,23 +39,8 @@ test.describe('Extension Onboarding Flow', () => {
timeout: 5000,
})
- // Check for service workers or background pages
- const backgroundPages = context.backgroundPages()
+ // MV3 extensions use service workers
const serviceWorkers = context.serviceWorkers()
-
- // Either background pages or service workers should exist
- const hasBackground = backgroundPages.length > 0 || serviceWorkers.length > 0
- expect(hasBackground).toBe(true)
-
- if (serviceWorkers.length > 0) {
- // For service workers, we can't evaluate directly
- } else if (backgroundPages.length > 0) {
- // Verify background script is running
- const background = backgroundPages[0]
- const hasBackgroundStore = await background?.evaluate(() => {
- return 'backgroundStore' in window
- })
- expect(hasBackgroundStore).toBe(true)
- }
+ expect(serviceWorkers.length).toBeGreaterThan(0)
})
})
diff --git a/apps/extension/e2e/utils/extension-helpers.ts b/apps/extension/e2e/utils/extension-helpers.ts
index 8e6533b6f89..73b9b51c119 100644
--- a/apps/extension/e2e/utils/extension-helpers.ts
+++ b/apps/extension/e2e/utils/extension-helpers.ts
@@ -13,27 +13,9 @@ export async function waitForBackgroundReady(context: BrowserContext): Promise 0) {
- const background = backgroundPages[0]
- const isReady = await background
- ?.evaluate(() => {
- // Check if the background store is initialized
- return typeof window !== 'undefined' && 'backgroundStore' in window
- })
- .catch(() => false)
-
- if (isReady) {
- return
- }
- }
-
- // Also check for service workers (modern extensions use these)
+ // MV3 extensions use service workers instead of background pages
const serviceWorkers = context.serviceWorkers()
if (serviceWorkers.length > 0) {
- // Service workers are ready if they exist
- // We can't evaluate inside them like background pages
return
}
diff --git a/apps/extension/e2e/utils/onboarding-helpers.ts b/apps/extension/e2e/utils/onboarding-helpers.ts
index 373cfb31f7c..fccb94d9d81 100644
--- a/apps/extension/e2e/utils/onboarding-helpers.ts
+++ b/apps/extension/e2e/utils/onboarding-helpers.ts
@@ -71,6 +71,7 @@ export async function completeOnboarding(context: BrowserContext, existingOnboar
// Find button with "Continue" text
const buttons = document.querySelectorAll('button')
for (const button of buttons) {
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- biome-parity: oxlint is stricter here
if (button.textContent?.includes('Continue') && !button.hasAttribute('disabled')) {
return true
}
diff --git a/apps/extension/e2e/utils/wait-for-extension.ts b/apps/extension/e2e/utils/wait-for-extension.ts
index 5294cc1c6a7..3918a16963b 100644
--- a/apps/extension/e2e/utils/wait-for-extension.ts
+++ b/apps/extension/e2e/utils/wait-for-extension.ts
@@ -1,4 +1,4 @@
-/** biome-ignore-all lint/suspicious/noExplicitAny: e2e test file */
+/* oxlint-disable typescript/no-explicit-any -- e2e test file */
import type { BrowserContext } from '@playwright/test'
import { sleep } from 'utilities/src/time/timing'
@@ -8,11 +8,13 @@ export async function waitForExtensionLoad(
timeout?: number
waitForOnboarding?: boolean
},
+ // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here
): Promise<{ extensionId: string; onboardingPage?: any }> {
const timeout = options?.timeout ?? 30000
const startTime = Date.now()
let extensionId: string | undefined
+ // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here
let onboardingPage: any
while (Date.now() - startTime < timeout) {
@@ -29,19 +31,7 @@ export async function waitForExtensionLoad(
}
}
- // Check background pages
- if (!extensionId) {
- const backgroundPages = context.backgroundPages()
- for (const page of backgroundPages) {
- const url = page.url()
- if (url.startsWith('chrome-extension://')) {
- extensionId = url.split('/')[2]
- break
- }
- }
- }
-
- // Check service workers
+ // Check service workers (MV3 extensions use service workers, not background pages)
if (!extensionId) {
const workers = context.serviceWorkers()
for (const worker of workers) {
diff --git a/apps/extension/env.d.ts b/apps/extension/env.d.ts
new file mode 100644
index 00000000000..660ba371727
--- /dev/null
+++ b/apps/extension/env.d.ts
@@ -0,0 +1,16 @@
+/* oxlint-disable typescript/no-namespace -- required to define process.env type */
+
+declare global {
+ namespace NodeJS {
+ // All process.env values used by this package should be listed here
+ interface ProcessEnv {
+ NODE_ENV?: 'development' | 'production' | 'test'
+ BUILD_ENV?: string
+ CI?: string
+ VERSION?: string
+ WDYR?: string
+ }
+ }
+}
+
+export {}
diff --git a/apps/extension/jest-setup.js b/apps/extension/jest-setup.js
index beb80a32847..7ac54c793eb 100644
--- a/apps/extension/jest-setup.js
+++ b/apps/extension/jest-setup.js
@@ -2,10 +2,9 @@ import 'utilities/jest-package-mocks'
import 'uniswap/jest-package-mocks'
import 'wallet/jest-package-mocks'
import 'config/jest-presets/ui/ui-package-mocks'
-import 'react-native-gesture-handler/jestSetup';
-
+import 'react-native-gesture-handler/jestSetup'
import { chrome } from 'jest-chrome'
-import { AppearanceSettingType } from 'wallet/src/features/appearance/slice'
+import { AppearanceSettingType } from 'uniswap/src/features/appearance/slice'
process.env.IS_UNISWAP_EXTENSION = true
@@ -13,20 +12,20 @@ const ignoreLogs = {
error: [
// We need to use _persist property to ensure that the state is properly
// rehydrated (https://github.com/Uniswap/universe/pull/7502/files#r1566259088)
- 'Unexpected key "_persist" found in previous state received by the reducer.'
- ]
+ 'Unexpected key "_persist" found in previous state received by the reducer.',
+ ],
}
// Ignore certain logs that are expected during tests.
Object.entries(ignoreLogs).forEach(([method, messages]) => {
const key = method
const originalMethod = console[key]
- console[key] = ((...args) => {
+ console[key] = (...args) => {
if (messages.some((message) => args.some((arg) => typeof arg === 'string' && arg.startsWith(message)))) {
return
}
originalMethod(...args)
- })
+ }
})
globalThis.matchMedia =
@@ -51,7 +50,7 @@ global.chrome = {
...chrome,
i18n: {
...global.chrome.i18n,
- getUILanguage: jest.fn().mockReturnValue(MOCK_LANGUAGE)
+ getUILanguage: jest.fn().mockReturnValue(MOCK_LANGUAGE),
},
storage: {
...chrome.storage,
@@ -83,16 +82,16 @@ global.chrome = {
callback()
}
return Promise.resolve()
- })
- }
- }
+ }),
+ },
+ },
}
jest.mock('src/app/navigation/utils', () => ({
useExtensionNavigation: () => ({
navigateTo: jest.fn(),
navigateBack: jest.fn(),
- })
+ }),
}))
jest.mock('wallet/src/features/focus/useIsFocused', () => {
@@ -100,7 +99,7 @@ jest.mock('wallet/src/features/focus/useIsFocused', () => {
})
const mockAppearanceSetting = AppearanceSettingType.System
-jest.mock('wallet/src/features/appearance/hooks', () => {
+jest.mock('uniswap/src/features/appearance/hooks', () => {
return {
useCurrentAppearanceSetting: () => mockAppearanceSetting,
useSelectedColorScheme: () => 'light',
diff --git a/apps/extension/jest.config.js b/apps/extension/jest.config.js
index cf7a5007b46..3093d961033 100644
--- a/apps/extension/jest.config.js
+++ b/apps/extension/jest.config.js
@@ -1,17 +1,6 @@
const preset = require('../../config/jest-presets/jest/jest-preset')
-const fileExtensions = [
- 'eot',
- 'gif',
- 'jpeg',
- 'jpg',
- 'otf',
- 'png',
- 'ttf',
- 'woff',
- 'woff2',
- 'mp4',
-]
+const fileExtensions = ['eot', 'gif', 'jpeg', 'jpg', 'otf', 'png', 'ttf', 'woff', 'woff2', 'mp4']
module.exports = {
...preset,
@@ -21,31 +10,18 @@ module.exports = {
'babel-jest',
{
configFile: './src/test/babel.config.js',
- }
+ },
],
},
moduleNameMapper: {
...preset.moduleNameMapper,
'^react-native$': 'react-native-web',
},
- moduleFileExtensions: [
- 'web.js',
- 'web.jsx',
- 'web.ts',
- 'web.tsx',
- ...fileExtensions,
- ...preset.moduleFileExtensions,
- ],
- resolver: "/src/test/jest-resolver.js",
+ moduleFileExtensions: ['web.js', 'web.jsx', 'web.ts', 'web.tsx', ...fileExtensions, ...preset.moduleFileExtensions],
+ resolver: '/src/test/jest-resolver.js',
displayName: 'Extension Wallet',
- testMatch: [
- '/src/**/*.(spec|test).[jt]s?(x)',
- '/config/**/*.(spec|test).[jt]s?(x)',
- ],
- testPathIgnorePatterns: [
- ...preset.testPathIgnorePatterns,
- '/e2e/',
- ],
+ testMatch: ['/src/**/*.(spec|test).[jt]s?(x)', '/config/**/*.(spec|test).[jt]s?(x)'],
+ testPathIgnorePatterns: [...preset.testPathIgnorePatterns, '/e2e/'],
collectCoverageFrom: [
'src/app/**/*.{js,ts,tsx}',
'src/background/**/*.{js,ts,tsx}',
@@ -59,8 +35,5 @@ module.exports = {
lines: 0,
},
},
- setupFiles: [
- '../../config/jest-presets/jest/setup.js',
- './jest-setup.js',
- ],
+ setupFiles: ['../../config/jest-presets/jest/setup.js', './jest-setup.js'],
}
diff --git a/apps/extension/package.json b/apps/extension/package.json
index bfd7f0ff2fd..b8d65bfbfd4 100644
--- a/apps/extension/package.json
+++ b/apps/extension/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"browserslist": "last 2 chrome versions",
"dependencies": {
- "@apollo/client": "3.10.4",
+ "@apollo/client": "3.11.10",
"@datadog/browser-rum": "5.23.3",
"@ethersproject/bignumber": "5.7.0",
"@ethersproject/providers": "5.7.2",
@@ -12,36 +12,36 @@
"@metamask/rpc-errors": "6.2.1",
"@reduxjs/toolkit": "1.9.3",
"@svgr/webpack": "8.0.1",
- "@tamagui/core": "1.125.17",
- "@tanstack/react-query": "5.77.2",
+ "@tamagui/core": "1.136.1",
+ "@tanstack/react-query": "5.90.20",
"@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.43.0",
- "@uniswap/client-embeddedwallet": "0.0.16",
- "@uniswap/uniswapx-sdk": "3.0.0-beta.7",
- "@uniswap/universal-router-sdk": "4.19.5",
- "@uniswap/v3-sdk": "3.25.2",
- "@uniswap/v4-sdk": "1.21.2",
+ "@uniswap/client-notification-service": "0.0.11",
+ "@uniswap/sdk-core": "7.12.1",
+ "@uniswap/universal-router-sdk": "4.33.0",
+ "@uniswap/v3-sdk": "3.29.1",
+ "@uniswap/v4-sdk": "1.29.1",
"@universe/api": "workspace:^",
"@universe/gating": "workspace:^",
+ "@universe/notifications": "workspace:^",
+ "@universe/sessions": "workspace:^",
"@wxt-dev/module-react": "1.1.3",
- "confusing-browser-globals": "1.0.11",
"dotenv-webpack": "8.0.1",
- "eslint-plugin-rulesdir": "0.2.2",
"ethers": "5.7.2",
"eventemitter3": "5.0.1",
"i18next": "23.10.0",
"node-polyfill-webpack-plugin": "2.0.1",
- "react": "18.3.1",
- "react-dom": "18.3.1",
+ "react": "19.0.3",
+ "react-dom": "19.0.3",
"react-i18next": "14.1.0",
- "react-native": "0.77.2",
- "react-native-gesture-handler": "2.22.1",
- "react-native-reanimated": "3.16.7",
- "react-native-svg": "15.11.2",
+ "react-native": "0.79.5",
+ "react-native-gesture-handler": "2.24.0",
+ "react-native-reanimated": "3.19.3",
+ "react-native-svg": "15.13.0",
"react-native-web": "0.19.13",
"react-qr-code": "2.0.12",
"react-redux": "8.0.5",
- "react-router": "7.12.0",
+ "react-router": "7.6.3",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-persist": "6.0.0",
@@ -54,37 +54,37 @@
"uniswap": "workspace:^",
"utilities": "workspace:^",
"uuid": "9.0.0",
- "vite": "npm:rolldown-vite@7.0.10",
+ "vite": "7.3.1",
"vite-plugin-commonjs": "0.10.4",
- "vite-plugin-node-polyfills": "0.23.0",
+ "vite-plugin-node-polyfills": "0.24.0",
"vite-plugin-svgr": "4.3.0",
"vite-tsconfig-paths": "5.1.4",
"wallet": "workspace:^",
"wxt": "0.20.8",
- "zod": "3.22.4"
+ "zod": "4.3.6",
+ "zustand": "5.0.6"
},
"devDependencies": {
- "@playwright/test": "1.49.1",
+ "@playwright/test": "1.58.2",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@testing-library/dom": "10.4.0",
- "@testing-library/react": "16.1.0",
+ "@testing-library/react": "16.3.0",
"@types/chrome": "0.0.304",
"@types/jest": "29.5.14",
"@types/ms": "0.7.31",
"@types/node": "22.13.1",
- "@types/react": "18.3.18",
- "@types/react-dom": "18.3.1",
+ "@types/react": "19.0.10",
+ "@types/react-dom": "19.0.6",
"@types/redux-logger": "3.0.9",
"@types/redux-persist-webextension-storage": "1.0.3",
"@types/ua-parser-js": "0.7.31",
- "@uniswap/eslint-config": "workspace:^",
- "@welldone-software/why-did-you-render": "8.0.1",
+ "@typescript/native-preview": "7.0.0-dev.20260311.1",
+ "@welldone-software/why-did-you-render": "10.0.1",
"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",
"jest-chrome": "0.8.0",
"jest-environment-jsdom": "29.5.0",
@@ -95,17 +95,19 @@
"serve": "14.2.4",
"style-loader": "3.3.2",
"swc-loader": "0.2.6",
- "tamagui-loader": "1.125.17",
- "typescript": "5.3.3",
- "webpack": "5.94.0",
+ "tamagui-loader": "1.136.1",
+ "typescript": "5.8.3",
+ "webpack": "5.90.0",
"webpack-cli": "5.1.4",
- "webpack-dev-server": "5.2.1"
+ "webpack-dev-server": "4.15.1"
},
"private": true,
"scripts": {
"build:firefox": "nx build:firefox extension",
"build:production": "nx build:production extension",
"build:wxt": "nx build:wxt extension",
+ "check": "nx check extension",
+ "check:fast": "nx check:fast extension",
"check:circular": "nx check:circular extension",
"check:deps:usage": "nx check:deps:usage extension",
"clean": "nx clean extension",
@@ -113,8 +115,8 @@
"dev:firefox": "nx dev:firefox extension",
"env:local:download": "nx env:local:download extension",
"env:local:upload": "nx env:local:upload extension",
- "lint:biome": "nx lint:biome extension",
- "lint:biome:fix": "nx lint:biome:fix extension",
+ "format": "nx format extension",
+ "format:check": "nx format:check extension",
"lint": "nx lint extension",
"lint:fix": "nx lint:fix extension",
"postinstall": "nx postinstall extension",
@@ -136,7 +138,10 @@
"playwright:ui": "nx playwright:ui extension",
"e2e": "nx e2e extension",
"e2e:smoke": "nx e2e:smoke extension",
- "e2e:ui": "nx e2e:ui extension"
+ "e2e:ui": "nx e2e:ui extension",
+ "validate:build": "nx validate:build extension",
+ "validate:build:dev": "nx validate:build:dev extension",
+ "validate:build:prod": "nx validate:build:prod extension"
},
"nx": {
"includedScripts": []
diff --git a/apps/extension/project.json b/apps/extension/project.json
index 95a9a62b8cf..8938a0b735b 100644
--- a/apps/extension/project.json
+++ b/apps/extension/project.json
@@ -1,4 +1,5 @@
{
+ "tags": ["scope:extension", "type:app"],
"targets": {
"build": {
"executor": "nx:noop",
@@ -23,17 +24,12 @@
}
},
"check:circular": {
- "command": "concurrently \"../../scripts/check-circular-imports.sh ./src/entrypoints/sidepanel/main.tsx 0\" \"../../scripts/check-circular-imports.sh ./src/entrypoints/onboarding/main.tsx 0\" \"../../scripts/check-circular-imports.sh ./src/entrypoints/unitagClaim/main.tsx 0\"",
- "options": {
- "cwd": "{projectRoot}"
- }
- },
- "check:deps:usage": {
- "command": "depcheck",
+ "command": "bunx madge --circular ./src/entrypoints/sidepanel/main.tsx ./src/entrypoints/onboarding/main.tsx ./src/entrypoints/unitagClaim/main.tsx",
"options": {
"cwd": "{projectRoot}"
}
},
+ "check:deps:usage": {},
"clean": {
"command": "wxt clean",
"options": {
@@ -64,22 +60,18 @@
"cwd": "{projectRoot}"
}
},
+ "format": {},
+ "format:check": {},
+ "lint:oxlint": {},
+ "lint:oxlint:fix": {},
+ "lint:oxlint:fast": {},
+ "lint:typeaware-custom": {
+ "command": "bun config/oxlint-plugins/typeaware-custom.ts apps/extension"
+ },
"lint": {},
"lint:fix": {},
- "lint:biome": {},
- "lint:biome:fix": {},
- "lint:eslint": {
- "command": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --ext ts,tsx --max-warnings=0",
- "options": {
- "cwd": "{projectRoot}"
- }
- },
- "lint:eslint:fix": {
- "command": "NODE_OPTIONS=--max-old-space-size=8192 eslint . --ext ts,tsx --fix",
- "options": {
- "cwd": "{projectRoot}"
- }
- },
+ "check": {},
+ "check:fast": {},
"postinstall": {
"command": "wxt prepare",
"options": {
@@ -90,7 +82,15 @@
"command": "wxt prepare",
"options": {
"cwd": "{projectRoot}"
- }
+ },
+ "cache": true,
+ "inputs": [
+ "{projectRoot}/wxt.config.ts",
+ "{projectRoot}/config/**",
+ "{projectRoot}/src/entrypoints/**/*",
+ "{projectRoot}/package.json"
+ ],
+ "outputs": ["{projectRoot}/.wxt/types", "{projectRoot}/.wxt/tsconfig.json", "{projectRoot}/.wxt/wxt.d.ts"]
},
"snapshots": {
"command": "jest -u",
@@ -141,6 +141,8 @@
}
},
"typecheck": {},
+ "typecheck:tsc": {},
+ "typecheck:tsgo": {},
"zip": {
"command": "wxt zip",
"options": {
@@ -192,6 +194,26 @@
"e2e:ui": {
"command": "nx playwright:ui extension",
"dependsOn": ["build:e2e"]
+ },
+ "validate:build": {
+ "command": "bunx tsx scripts/validateBuildOutput.ts",
+ "options": {
+ "cwd": "{projectRoot}"
+ },
+ "dependsOn": ["build:wxt"]
+ },
+ "validate:build:dev": {
+ "command": "bunx tsx scripts/validateBuildOutput.ts --dev",
+ "options": {
+ "cwd": "{projectRoot}"
+ }
+ },
+ "validate:build:prod": {
+ "command": "bunx tsx scripts/validateBuildOutput.ts --prod",
+ "options": {
+ "cwd": "{projectRoot}"
+ },
+ "dependsOn": ["build:wxt"]
}
}
}
diff --git a/apps/extension/scripts/validateBuildOutput.ts b/apps/extension/scripts/validateBuildOutput.ts
new file mode 100644
index 00000000000..ec4c6fcda3d
--- /dev/null
+++ b/apps/extension/scripts/validateBuildOutput.ts
@@ -0,0 +1,88 @@
+/* oxlint-disable no-console -- CLI script requires console output */
+/**
+ * Validates extension build output for common issues.
+ *
+ * Checks for:
+ * - __vite_browser_external markers (indicates Node.js modules were externalized)
+ */
+import * as fs from 'node:fs'
+import * as path from 'node:path'
+
+const BUILD_DIRS = [
+ '.output/chrome-mv3-dev', // dev build (bun extension start)
+ '.output/chrome-mv3', // production build (bun extension build:production)
+]
+
+// Can be overridden via CLI arg: --dev or --prod
+const args = process.argv.slice(2)
+const devOnly = args.includes('--dev')
+const prodOnly = args.includes('--prod')
+
+// Support WXT_ABSOLUTE_OUTDIR for absolute path builds (e.g., bun extension start:absolute)
+const absoluteOutDir = process.env['WXT_ABSOLUTE_OUTDIR']
+
+const dirsToCheck = absoluteOutDir
+ ? [absoluteOutDir]
+ : devOnly
+ ? ['.output/chrome-mv3-dev']
+ : prodOnly
+ ? ['.output/chrome-mv3']
+ : BUILD_DIRS
+
+const BACKGROUND_SCRIPT = 'background.js'
+
+// Patterns that indicate problematic externalization
+const FORBIDDEN_PATTERNS = [
+ {
+ pattern: '__vite_browser_external',
+ message:
+ 'Node.js module externalization detected. A dependency is importing Node.js built-ins (like "util", "fs", etc.) that cannot run in the browser. Check recent import changes in background script entry points.',
+ },
+]
+
+function validateBuild(): boolean {
+ let buildDir: string | null = null
+
+ // Find existing build directory
+ for (const dir of dirsToCheck) {
+ // For absolute paths, use directly; for relative paths, resolve from project root
+ const fullPath = path.isAbsolute(dir) ? dir : path.join(__dirname, '..', dir)
+ if (fs.existsSync(fullPath)) {
+ buildDir = fullPath
+ break
+ }
+ }
+
+ if (!buildDir) {
+ console.error('No build output found. Run `bun build:wxt` first.')
+ process.exit(1)
+ }
+
+ const backgroundPath = path.join(buildDir, BACKGROUND_SCRIPT)
+
+ if (!fs.existsSync(backgroundPath)) {
+ console.error(`Background script not found at ${backgroundPath}`)
+ process.exit(1)
+ }
+
+ const content = fs.readFileSync(backgroundPath, 'utf-8')
+ let hasErrors = false
+
+ for (const { pattern, message } of FORBIDDEN_PATTERNS) {
+ if (content.includes(pattern)) {
+ console.error(`\n❌ BUILD VALIDATION FAILED`)
+ console.error(`Pattern found: "${pattern}"`)
+ console.error(`\n${message}\n`)
+ hasErrors = true
+ }
+ }
+
+ if (hasErrors) {
+ process.exit(1)
+ }
+
+ console.log('✅ Build validation passed')
+ return true
+}
+
+validateBuild()
diff --git a/apps/extension/src/app/Global.css b/apps/extension/src/app/Global.css
index 0fc71502544..0e392a9ee67 100644
--- a/apps/extension/src/app/Global.css
+++ b/apps/extension/src/app/Global.css
@@ -6,6 +6,17 @@ html {
font-variant-ligatures: no-contextual;
}
+/* Theme-aware background colors using Tamagui theme classes */
+.t_light body,
+.t_light html {
+ background-color: #fff;
+}
+
+.t_dark body,
+.t_dark html {
+ background-color: #131313;
+}
+
#root {
height: 100vh;
display: flex;
diff --git a/apps/extension/src/app/apollo.tsx b/apps/extension/src/app/apollo.tsx
index e006c3ed06a..3955e078a4f 100644
--- a/apps/extension/src/app/apollo.tsx
+++ b/apps/extension/src/app/apollo.tsx
@@ -2,7 +2,7 @@ import { ApolloProvider } from '@apollo/client/react/context'
import { PropsWithChildren } from 'react'
import { localStorage } from 'redux-persist-webextension-storage'
import { getReduxStore } from 'src/store/store'
-// biome-ignore lint/style/noRestrictedImports: Direct wallet import needed for Apollo client setup in extension context
+// oxlint-disable-next-line no-restricted-imports -- Direct wallet import needed for Apollo client setup in extension context
import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApolloClient'
// Extension local storage has 10 MB limit, so we want to be very careful to leave enough space for the redux store + any other data that we might want to store in local storage
diff --git a/apps/extension/src/app/components/AutoLockProvider.tsx b/apps/extension/src/app/components/AutoLockProvider.tsx
index 8f26d940352..d28e0bfd971 100644
--- a/apps/extension/src/app/components/AutoLockProvider.tsx
+++ b/apps/extension/src/app/components/AutoLockProvider.tsx
@@ -1,73 +1,100 @@
-import { FeatureFlags, useFeatureFlag } from '@universe/gating'
-import { PropsWithChildren, useEffect } from 'react'
+import { PropsWithChildren, useEffect, useRef } from 'react'
import { useSelector } from 'react-redux'
-import { ExtensionState } from 'src/store/extensionReducer'
-import { useIsChromeWindowFocusedWithTimeout } from 'uniswap/src/extension/useIsChromeWindowFocused'
-import { deviceAccessTimeoutToMs } from 'uniswap/src/features/settings/constants'
-import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
-import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
+import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
+import { useIsChromeWindowFocused } from 'uniswap/src/extension/useIsChromeWindowFocused'
+import { selectDeviceAccessTimeoutMinutes } from 'uniswap/src/features/settings/selectors'
import { logger } from 'utilities/src/logger/logger'
-import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
-const AUTO_LOCK_ALARM_NAME = 'AutoLockAlarm'
+export const AUTO_LOCK_ALARM_NAME = 'AutoLockAlarm'
/**
- * AutoLockProvider monitors window focus and automatically locks the wallet
- * after the configured inactivity timeout period.
+ * Helper to safely clear the auto-lock alarm with error handling
+ */
+function clearAutoLockAlarm(reason: string): void {
+ try {
+ // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here
+ chrome.alarms.clear(AUTO_LOCK_ALARM_NAME)
+ logger.debug('AutoLockProvider', 'clearAutoLockAlarm', reason)
+ } catch (error) {
+ logger.error(error, {
+ tags: { file: 'AutoLockProvider', function: 'clearAutoLockAlarm' },
+ extra: { reason },
+ })
+ }
+}
+
+/**
+ * Helper to safely create the auto-lock alarm with error handling
+ */
+function createAutoLockAlarm(delayInMinutes: number): void {
+ try {
+ // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here
+ chrome.alarms.create(AUTO_LOCK_ALARM_NAME, { delayInMinutes })
+ logger.debug('AutoLockProvider', 'createAutoLockAlarm', `Scheduled auto-lock alarm for ${delayInMinutes} minutes`)
+ } catch (error) {
+ logger.error(error, {
+ tags: { file: 'AutoLockProvider', function: 'createAutoLockAlarm' },
+ extra: { delayInMinutes },
+ })
+ }
+}
+
+/**
+ * AutoLockProvider schedules chrome alarms to automatically lock the wallet
+ * after the configured timeout period when the sidebar is not focused.
*
- * This component should be placed high in the component tree to ensure
- * it's always active when the extension is running.
+ * Uses chrome.alarms API which persists even when the extension is closed,
+ * ensuring reliable auto-lock behavior.
*/
export function AutoLockProvider({ children }: PropsWithChildren): JSX.Element {
- const deviceAccessTimeout = useSelector((state: ExtensionState) => state.userSettings.deviceAccessTimeout)
- const useAlarmsApi = useFeatureFlag(FeatureFlags.UseAlarmsApi)
- const timeoutMs = deviceAccessTimeoutToMs(deviceAccessTimeout)
+ const delayInMinutes = useSelector(selectDeviceAccessTimeoutMinutes)
+ const isWalletUnlocked = useIsWalletUnlocked()
+ const isChromeWindowFocused = useIsChromeWindowFocused()
- // Use the window focus hook with the configured timeout
- // If timeoutMs is undefined (Never setting), use a very large number to effectively disable
- const isChromeWindowFocused = useIsChromeWindowFocusedWithTimeout(timeoutMs ?? Number.MAX_SAFE_INTEGER)
+ // Ref to track previous focus state
+ const prevFocusedRef = useRef(true)
+ // Ref to track previous unlock state
+ const prevUnlockedRef = useRef(null)
- // Maintain chrome.alarms usage behind feature flag
+ // On mount: Clear any existing alarm (sidebar just opened)
useEffect(() => {
- if (useAlarmsApi) {
- chrome.alarms.create(AUTO_LOCK_ALARM_NAME, {
- delayInMinutes: 1000,
- })
+ clearAutoLockAlarm('Cleared auto-lock alarm (sidebar opened)')
+ }, [])
+
+ useEffect(() => {
+ // Skip if timeout not configured (Never)
+ if (delayInMinutes === undefined) {
+ clearAutoLockAlarm('Cleared auto-lock alarm (timeout not configured)')
+ return
}
- return () => {
- chrome.alarms.clear(AUTO_LOCK_ALARM_NAME)
+ const prevFocused = prevFocusedRef.current
+ const prevUnlocked = prevUnlockedRef.current
+ prevFocusedRef.current = isChromeWindowFocused
+ prevUnlockedRef.current = isWalletUnlocked
+
+ // Skip first render for unlock state
+ if (prevUnlocked === null) {
+ return
}
- }, [useAlarmsApi])
- useEffect(() => {
- // Only lock if timeout is configured (not "Never")
- if (timeoutMs === undefined) {
+ // Clear alarm when wallet state changes (locked or unlocked)
+ if (prevUnlocked !== isWalletUnlocked) {
+ clearAutoLockAlarm(`Cleared auto-lock alarm (wallet ${isWalletUnlocked ? 'unlocked' : 'locked'})`)
return
}
- if (!isChromeWindowFocused) {
- const lockWallet = async (): Promise => {
- try {
- logger.debug('AutoLockProvider', 'lockWallet', 'Locking wallet due to inactivity')
- await Keyring.lock()
- sendAnalyticsEvent(ExtensionEventName.ChangeLockedState, {
- locked: true,
- location: 'background',
- })
- } catch (error) {
- logger.error(error, {
- tags: {
- file: 'AutoLockProvider.tsx',
- function: 'lockWallet',
- },
- })
- }
- }
+ // When window loses focus AND wallet is unlocked: schedule alarm
+ if (prevFocused && !isChromeWindowFocused && isWalletUnlocked) {
+ createAutoLockAlarm(delayInMinutes)
+ return
+ }
- lockWallet()
+ // When window regains focus: clear alarm
+ if (!prevFocused && isChromeWindowFocused) {
+ clearAutoLockAlarm('Cleared auto-lock alarm (window focused)')
}
- }, [isChromeWindowFocused, timeoutMs])
+ }, [isChromeWindowFocused, isWalletUnlocked, delayInMinutes])
return <>{children}>
}
diff --git a/apps/extension/src/app/components/Input.tsx b/apps/extension/src/app/components/Input.tsx
index 940e4e1bf9f..41ff4a1dc03 100644
--- a/apps/extension/src/app/components/Input.tsx
+++ b/apps/extension/src/app/components/Input.tsx
@@ -11,7 +11,7 @@ export type InputProps = {
export type Input = TamaguiInput
-export const Input = forwardRef(function _Input(
+export const Input = forwardRef(function InputInner(
{ large = false, hideInput = false, centered = false, ...rest }: InputProps,
ref,
): JSX.Element {
diff --git a/apps/extension/src/app/components/Trace/TraceUserProperties.tsx b/apps/extension/src/app/components/Trace/TraceUserProperties.tsx
index 249763fc1e9..fda8745533d 100644
--- a/apps/extension/src/app/components/Trace/TraceUserProperties.tsx
+++ b/apps/extension/src/app/components/Trace/TraceUserProperties.tsx
@@ -1,4 +1,7 @@
import { datadogRum } from '@datadog/browser-rum'
+import { useQuery } from '@tanstack/react-query'
+import { provideUniswapIdentifierService } from '@universe/api'
+import { uniswapIdentifierQuery } from '@universe/sessions'
import { useEffect } from 'react'
import { useIsDarkMode } from 'ui/src'
import { DisplayNameType } from 'uniswap/src/features/accounts/types'
@@ -7,7 +10,7 @@ import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks'
import { useCurrentLanguage } from 'uniswap/src/features/language/hooks'
import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks'
import { ExtensionUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user'
-// biome-ignore lint/style/noRestrictedImports: Direct analytics import required for user property tracking
+// oxlint-disable-next-line no-restricted-imports -- Direct analytics import required for user property tracking
import { analytics } from 'utilities/src/telemetry/analytics/analytics'
import { useGatingUserPropertyUsernames } from 'wallet/src/features/gating/userPropertyHooks'
import {
@@ -30,6 +33,8 @@ export function TraceUserProperties(): null {
const { isTestnetModeEnabled } = useEnabledChains()
const displayName = useDisplayName(activeAccount?.address)
+ const { data: uniswapIdentifier } = useQuery(uniswapIdentifierQuery(provideUniswapIdentifierService))
+
useGatingUserPropertyUsernames()
// Set user properties for datadog
@@ -38,6 +43,12 @@ export function TraceUserProperties(): null {
datadogRum.setUserProperty(ExtensionUserPropertyName.ActiveWalletAddress, activeAccount?.address)
}, [activeAccount?.address])
+ useEffect(() => {
+ if (uniswapIdentifier) {
+ datadogRum.setUserProperty(ExtensionUserPropertyName.UniswapIdentifier, uniswapIdentifier)
+ }
+ }, [uniswapIdentifier])
+
// Set user properties for amplitude
useEffect(() => {
diff --git a/apps/extension/src/app/components/buttons/OpenSidebarButton.tsx b/apps/extension/src/app/components/buttons/OpenSidebarButton.tsx
new file mode 100644
index 00000000000..a0573044c3a
--- /dev/null
+++ b/apps/extension/src/app/components/buttons/OpenSidebarButton.tsx
@@ -0,0 +1,29 @@
+import { useTranslation } from 'react-i18next'
+import { Button, Flex } from 'ui/src'
+import { ArrowRight } from 'ui/src/components/icons/ArrowRight'
+
+export function OpenSidebarButton({
+ openedSideBar,
+ handleOpenSidebar,
+ handleOpenWebApp,
+}: {
+ openedSideBar: boolean
+ handleOpenSidebar: () => Promise
+ handleOpenWebApp: () => Promise
+}) {
+ const { t } = useTranslation()
+ return (
+
+ : undefined}
+ iconPosition="after"
+ size="large"
+ variant={openedSideBar ? 'branded' : 'default'}
+ emphasis={openedSideBar ? 'primary' : 'secondary'}
+ onPress={openedSideBar ? handleOpenWebApp : handleOpenSidebar}
+ >
+ {openedSideBar ? t('onboarding.complete.go_to_uniswap') : t('onboarding.complete.button')}
+
+
+ )
+}
diff --git a/apps/extension/src/app/components/buttons/OptionCard.tsx b/apps/extension/src/app/components/buttons/OptionCard.tsx
index 0273a209ea1..a535ee98249 100644
--- a/apps/extension/src/app/components/buttons/OptionCard.tsx
+++ b/apps/extension/src/app/components/buttons/OptionCard.tsx
@@ -18,7 +18,7 @@ export function OptionCard({
shadowColor="$shadowColor"
shadowOpacity={0.05}
shadowRadius={8}
- borderWidth={1}
+ borderWidth="$spacing1"
borderColor="$surface3"
borderRadius="$rounded20"
onPress={onPress}
@@ -33,7 +33,7 @@ export function OptionCard({
-
+ {title}
diff --git a/apps/extension/src/app/components/loading/SelectWalletSkeleton.tsx b/apps/extension/src/app/components/loading/SelectWalletSkeleton.tsx
index 012f3db4949..71a588543cf 100644
--- a/apps/extension/src/app/components/loading/SelectWalletSkeleton.tsx
+++ b/apps/extension/src/app/components/loading/SelectWalletSkeleton.tsx
@@ -5,7 +5,7 @@ import { WALLET_PREVIEW_CARD_MIN_HEIGHT } from 'wallet/src/components/WalletPrev
export function SelectWalletsSkeleton({ repeat = 3 }: { repeat?: number }): JSX.Element {
return (
- {/* eslint-disable-next-line max-params */}
+ {/* oxlint-disable-next-line max-params */}
{new Array(repeat).fill(null).map((_, i, { length }) => (
))}
diff --git a/apps/extension/src/app/components/loading/SkeletonBox.tsx b/apps/extension/src/app/components/loading/SkeletonBox.tsx
index faa8ebfc37d..eabca0b1e05 100644
--- a/apps/extension/src/app/components/loading/SkeletonBox.tsx
+++ b/apps/extension/src/app/components/loading/SkeletonBox.tsx
@@ -12,6 +12,6 @@ export function SkeletonBox({
height: number | string
borderRadius?: string
}): JSX.Element {
- // biome-ignore lint/correctness/noRestrictedElements: needed here
+ // oxlint-disable-next-line react/forbid-elements -- needed here
return
}
diff --git a/apps/extension/src/app/components/tabs/ActivityTab.tsx b/apps/extension/src/app/components/tabs/ActivityTab.tsx
index 95ca43b900f..bb2d0e200dc 100644
--- a/apps/extension/src/app/components/tabs/ActivityTab.tsx
+++ b/apps/extension/src/app/components/tabs/ActivityTab.tsx
@@ -3,7 +3,7 @@ import { Flex, Loader, ScrollView } from 'ui/src'
import { useInfiniteScroll } from 'utilities/src/react/useInfiniteScroll'
import { useActivityDataWallet } from 'wallet/src/features/activity/useActivityDataWallet'
-export const ActivityTab = memo(function _ActivityTab({
+export const ActivityTab = memo(function ActivityTabInner({
address,
skip,
}: {
diff --git a/apps/extension/src/app/components/tabs/NftsTab.tsx b/apps/extension/src/app/components/tabs/NftsTab.tsx
index 781cc7da8a0..1e34b3ad905 100644
--- a/apps/extension/src/app/components/tabs/NftsTab.tsx
+++ b/apps/extension/src/app/components/tabs/NftsTab.tsx
@@ -9,7 +9,7 @@ import { ElementName, SectionName } from 'uniswap/src/features/telemetry/constan
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useAccounts } from 'wallet/src/features/wallet/hooks'
-export const NftsTab = memo(function _NftsTab({ owner, skip }: { owner: Address; skip?: boolean }): JSX.Element {
+export const NftsTab = memo(function NftsTabInner({ owner, skip }: { owner: Address; skip?: boolean }): JSX.Element {
const accounts = useAccounts()
const renderNFTItem = useCallback(
diff --git a/apps/extension/src/app/context/SmartWalletNudgesContext.tsx b/apps/extension/src/app/context/SmartWalletNudgesContext.tsx
index 09f0f11af6c..1591c19be34 100644
--- a/apps/extension/src/app/context/SmartWalletNudgesContext.tsx
+++ b/apps/extension/src/app/context/SmartWalletNudgesContext.tsx
@@ -74,7 +74,7 @@ export function SmartWalletNudgesProvider({ children }: { children: ReactNode })
delegationStatus.status === SmartWalletDelegationAction.PromptUpgrade &&
!delegationStatus.loading
- // biome-ignore lint/correctness/useExhaustiveDependencies: delegationStatus is used in shouldShowNudge calculation above
+ // oxlint-disable-next-line react/exhaustive-deps -- delegationStatus is used in shouldShowNudge calculation above
useEffect(() => {
if (last5792DappInfo && shouldShowNudge) {
setDappInfo({
diff --git a/apps/extension/src/app/core/BaseAppContainer.tsx b/apps/extension/src/app/core/BaseAppContainer.tsx
index c29cba1056e..f2ff6aa9995 100644
--- a/apps/extension/src/app/core/BaseAppContainer.tsx
+++ b/apps/extension/src/app/core/BaseAppContainer.tsx
@@ -1,40 +1,103 @@
import { ApiInit, getEntryGatewayUrl, provideSessionService } from '@universe/api'
import {
+ getIsHashcashSolverEnabled,
getIsSessionServiceEnabled,
+ getIsSessionsPerformanceTrackingEnabled,
getIsSessionUpgradeAutoEnabled,
+ getIsTurnstileSolverEnabled,
useIsSessionServiceEnabled,
} from '@universe/gating'
import {
+ type ChallengeSolver,
+ ChallengeType,
createChallengeSolverService,
+ createHashcashMockSolver,
+ createHashcashSolver,
+ createHashcashWorkerChannel,
+ createPerformanceTracker,
createSessionInitializationService,
- SessionInitializationService,
+ createTurnstileMockSolver,
+ type SessionInitializationService,
} from '@universe/sessions'
-import { PropsWithChildren } from 'react'
+import { PropsWithChildren, useEffect } from 'react'
import { I18nextProvider } from 'react-i18next'
import { GraphqlProvider } from 'src/app/apollo'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
-import { SmartWalletNudgesProvider } from 'src/app/context/SmartWalletNudgesContext'
import { ExtensionStatsigProvider } from 'src/app/core/StatsigProvider'
-import { DatadogAppNameTag } from 'src/app/datadog'
+import { type DatadogAppNameTag } from 'src/app/datadog'
+import { onHashcashSolveCompleted, sessionInitAnalytics } from 'src/app/features/sessions/analytics'
+import { useOnCrashAppStateResetter } from 'src/store/appStateResetter'
import { getReduxStore } from 'src/store/store'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
+import { useCurrentLanguage } from 'uniswap/src/features/language/hooks'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
+import { getLocale } from 'uniswap/src/features/language/navigatorLocale'
import Trace from 'uniswap/src/features/telemetry/Trace'
-import i18n from 'uniswap/src/i18n'
+import i18n, { changeLanguage } from 'uniswap/src/i18n'
+import { getLogger } from 'utilities/src/logger/logger'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
-import { AccountsStoreContextProvider } from 'wallet/src/features/accounts/store/provider'
+import { StatsigUserIdentifiersUpdater } from 'wallet/src/features/gating/StatsigUserIdentifiersUpdater'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
-const provideSessionInitializationService = (): SessionInitializationService =>
- createSessionInitializationService({
+const provideSessionInitializationService = (): SessionInitializationService => {
+ // Create performance tracker with feature flag control
+ const performanceTracker = createPerformanceTracker({
+ getIsPerformanceTrackingEnabled: getIsSessionsPerformanceTrackingEnabled,
+ getNow: () => performance.now(),
+ })
+
+ const solvers = new Map()
+
+ if (getIsTurnstileSolverEnabled()) {
+ solvers.set(ChallengeType.TURNSTILE, createTurnstileMockSolver())
+ } else {
+ solvers.set(ChallengeType.TURNSTILE, createTurnstileMockSolver())
+ }
+
+ if (getIsHashcashSolverEnabled()) {
+ solvers.set(
+ ChallengeType.HASHCASH,
+ createHashcashSolver({
+ performanceTracker,
+ getWorkerChannel: () =>
+ createHashcashWorkerChannel({
+ getWorker: () =>
+ new Worker(
+ new URL('@universe/sessions/src/challenge-solvers/hashcash/worker/hashcash.worker.ts', import.meta.url),
+ { type: 'module' },
+ ),
+ }),
+ onSolveCompleted: onHashcashSolveCompleted,
+ getLogger,
+ }),
+ )
+ } else {
+ solvers.set(ChallengeType.HASHCASH, createHashcashMockSolver())
+ }
+
+ return createSessionInitializationService({
getSessionService: () =>
provideSessionService({
getBaseUrl: getEntryGatewayUrl,
getIsSessionServiceEnabled,
}),
- challengeSolverService: createChallengeSolverService(),
+ challengeSolverService: createChallengeSolverService({
+ solvers,
+ }),
+ performanceTracker,
getIsSessionUpgradeAutoEnabled,
+ getLogger,
+ analytics: sessionInitAnalytics,
})
+}
+
+/**
+ * Inner component that uses hooks requiring Redux context.
+ */
+function ErrorBoundaryWrapper({ children }: PropsWithChildren): JSX.Element {
+ const onCrashAppStateResetter = useOnCrashAppStateResetter()
+ return {children}
+}
function BaseAppContainerInner({ children }: PropsWithChildren): JSX.Element {
const isSessionServiceEnabled = useIsSessionServiceEnabled()
@@ -42,24 +105,22 @@ function BaseAppContainerInner({ children }: PropsWithChildren): JSX.Element {
return (
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
)
@@ -77,3 +138,13 @@ export function BaseAppContainer({
)
}
+
+function LanguageSync(): null {
+ const currentLanguage = useCurrentLanguage()
+
+ useEffect(() => {
+ changeLanguage(getLocale(currentLanguage)).catch(() => undefined)
+ }, [currentLanguage])
+
+ return null
+}
diff --git a/apps/extension/src/app/core/DevMenuModal.tsx b/apps/extension/src/app/core/DevMenuModal.tsx
index 098151b962e..20881f4c531 100644
--- a/apps/extension/src/app/core/DevMenuModal.tsx
+++ b/apps/extension/src/app/core/DevMenuModal.tsx
@@ -22,7 +22,7 @@ export function DevMenuModal(): JSX.Element {
p="$spacing4"
left="$spacing24"
zIndex={Number.MAX_SAFE_INTEGER}
- borderWidth={1}
+ borderWidth="$spacing1"
borderColor="$neutral2"
borderRadius="$rounded4"
cursor="pointer"
diff --git a/apps/extension/src/app/core/OnboardingApp.test.tsx b/apps/extension/src/app/core/OnboardingApp.test.tsx
index f5d23347a8c..b5506947974 100644
--- a/apps/extension/src/app/core/OnboardingApp.test.tsx
+++ b/apps/extension/src/app/core/OnboardingApp.test.tsx
@@ -7,7 +7,7 @@ jest.mock('wallet/src/features/transactions/contexts/WalletUniswapContext', () =
}))
describe('OnboardingApp', () => {
- // eslint-disable-next-line jest/expect-expect
+ // oxlint-disable-next-line jest/expect-expect
it('renders without error', async () => {
initializeReduxStore()
render()
diff --git a/apps/extension/src/app/core/OnboardingApp.tsx b/apps/extension/src/app/core/OnboardingApp.tsx
index bb3e054587c..5dafe03d522 100644
--- a/apps/extension/src/app/core/OnboardingApp.tsx
+++ b/apps/extension/src/app/core/OnboardingApp.tsx
@@ -1,7 +1,6 @@
import '@tamagui/core/reset.css'
import 'src/app/Global.css'
import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters
-
import { useEffect } from 'react'
import { createHashRouter, RouteObject, RouterProvider } from 'react-router'
import { PersistGate } from 'redux-persist/integration/react'
@@ -32,8 +31,8 @@ import { OnboardingWrapper } from 'src/app/features/onboarding/OnboardingWrapper
import { PasswordImport } from 'src/app/features/onboarding/PasswordImport'
import { ResetComplete } from 'src/app/features/onboarding/reset/ResetComplete'
import { OTPInput } from 'src/app/features/onboarding/scan/OTPInput'
-import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard'
import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
+import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { OnboardingNavigationProvider } from 'src/app/navigation/providers'
import { setRouter, setRouterState } from 'src/app/navigation/state'
@@ -43,6 +42,7 @@ import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebu
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension'
+import { AccountsStoreContextProvider } from 'wallet/src/features/accounts/store/provider'
import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext'
import { getReduxPersistor } from 'wallet/src/state/persistor'
@@ -196,8 +196,10 @@ export default function OnboardingApp(): JSX.Element {
-
-
+
+
+
+
diff --git a/apps/extension/src/app/core/PopupApp.tsx b/apps/extension/src/app/core/PopupApp.tsx
index 7e4b15af8d9..79fc8880a85 100644
--- a/apps/extension/src/app/core/PopupApp.tsx
+++ b/apps/extension/src/app/core/PopupApp.tsx
@@ -1,6 +1,5 @@
import '@tamagui/core/reset.css'
import 'src/app/Global.css'
-
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { createHashRouter, RouterProvider } from 'react-router'
diff --git a/apps/extension/src/app/core/SidebarApp.tsx b/apps/extension/src/app/core/SidebarApp.tsx
index eeadc95bfa9..0f5f366c2d4 100644
--- a/apps/extension/src/app/core/SidebarApp.tsx
+++ b/apps/extension/src/app/core/SidebarApp.tsx
@@ -1,6 +1,5 @@
import '@tamagui/core/reset.css'
import 'src/app/Global.css'
-
import { SharedEventName } from '@uniswap/analytics-events'
import { useEffect, useRef, useState } from 'react'
import { useDispatch } from 'react-redux'
@@ -19,12 +18,15 @@ import { SendFlow } from 'src/app/features/send/SendFlow'
import { BackupRecoveryPhraseScreen } from 'src/app/features/settings/BackupRecoveryPhrase/BackupRecoveryPhraseScreen'
import { DeviceAccessScreen } from 'src/app/features/settings/DeviceAccessScreen'
import { DevMenuScreen } from 'src/app/features/settings/DevMenuScreen'
+import { HashcashBenchmarkScreen } from 'src/app/features/settings/HashcashBenchmarkScreen'
+import { SessionsDebugScreen } from 'src/app/features/settings/SessionsDebugScreen'
import { SettingsManageConnectionsScreen } from 'src/app/features/settings/SettingsManageConnectionsScreen/SettingsManageConnectionsScreen'
import { RemoveRecoveryPhraseVerify } from 'src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseVerify'
import { RemoveRecoveryPhraseWallets } from 'src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseWallets'
import { ViewRecoveryPhraseScreen } from 'src/app/features/settings/SettingsRecoveryPhraseScreen/ViewRecoveryPhraseScreen'
import { SettingsScreen } from 'src/app/features/settings/SettingsScreen'
import { SettingsScreenWrapper } from 'src/app/features/settings/SettingsScreenWrapper'
+import { SettingsStorageScreen } from 'src/app/features/settings/SettingsStorageScreen'
import { SmartWalletSettingsScreen } from 'src/app/features/settings/SmartWalletSettingsScreen'
import { SwapFlowScreen } from 'src/app/features/swap/SwapFlowScreen'
import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
@@ -75,12 +77,22 @@ const router = createHashRouter([
path: SettingsRoutes.DeviceAccess,
element: ,
},
- isDevEnv()
- ? {
- path: SettingsRoutes.DevMenu,
- element: ,
- }
- : {},
+ ...(isDevEnv()
+ ? [
+ {
+ path: SettingsRoutes.DevMenu,
+ element: ,
+ },
+ {
+ path: SettingsRoutes.SessionsDebug,
+ element: ,
+ },
+ {
+ path: SettingsRoutes.HashcashBenchmark,
+ element: ,
+ },
+ ]
+ : []),
{
path: SettingsRoutes.ViewRecoveryPhrase,
element: ,
@@ -110,6 +122,10 @@ const router = createHashRouter([
path: SettingsRoutes.SmartWallet,
element: ,
},
+ {
+ path: SettingsRoutes.Storage,
+ element: ,
+ },
],
},
{
@@ -134,13 +150,14 @@ function useDappRequestPortListener(): void {
const [currentPortChannel, setCurrentPortChannel] = useState()
const [windowId, setWindowId] = useState()
- // biome-ignore lint/correctness/useExhaustiveDependencies: Only run on component mount for initial setup, disconnect cleanup is managed separately
+ // oxlint-disable-next-line react/exhaustive-deps -- Only run on component mount for initial setup, disconnect cleanup is managed separately
useEffect(() => {
chrome.windows.getCurrent((window) => {
setWindowId(window.id?.toString())
})
return () => currentPortChannel?.port.disconnect()
+ // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here
}, [])
useEffect(() => {
diff --git a/apps/extension/src/app/core/UnitagClaimApp.tsx b/apps/extension/src/app/core/UnitagClaimApp.tsx
index b8fc851fc8a..24c20b5f7da 100644
--- a/apps/extension/src/app/core/UnitagClaimApp.tsx
+++ b/apps/extension/src/app/core/UnitagClaimApp.tsx
@@ -1,6 +1,5 @@
import '@tamagui/core/reset.css'
import 'src/app/Global.css'
-
import { PropsWithChildren, useEffect } from 'react'
import { createHashRouter, Outlet, RouterProvider, useSearchParams } from 'react-router'
import { ErrorElement } from 'src/app/components/ErrorElement'
@@ -52,7 +51,7 @@ const router = createHashRouter([
* router/router state to a different file so it can be imported by those pages
*/
-// biome-ignore lint/suspicious/noExplicitAny: Router state object has dynamic structure from react-router
+// oxlint-disable-next-line typescript/no-explicit-any -- Router state object has dynamic structure from react-router
router.subscribe((state: any) => {
setRouterState(state)
})
@@ -77,7 +76,7 @@ function UnitagAppInner(): JSX.Element {
// needed to reload on address param change for hash router
router
.navigate(0)
- // biome-ignore lint/suspicious/noExplicitAny: Router state object has dynamic structure from react-router
+ // oxlint-disable-next-line typescript/no-explicit-any -- Router state object has dynamic structure from react-router
.catch((e: any) => logger.error(e, { tags: { file: 'UnitagClaimApp.tsx', function: 'UnitagClaimAppInner' } }))
}
}, [address, prevAddress])
diff --git a/apps/extension/src/app/features/accounts/AccountItem.tsx b/apps/extension/src/app/features/accounts/AccountItem.tsx
index 99eb7319601..9889dbdda0b 100644
--- a/apps/extension/src/app/features/accounts/AccountItem.tsx
+++ b/apps/extension/src/app/features/accounts/AccountItem.tsx
@@ -1,5 +1,5 @@
import { SharedEventName } from '@uniswap/analytics-events'
-import { BaseSyntheticEvent, useCallback, useMemo, useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal'
@@ -10,6 +10,8 @@ import { Flex, Text, TouchableArea } from 'ui/src'
import { CopySheets, Edit, Ellipsis, Globe, TrashFilled } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { AddressDisplay } from 'uniswap/src/components/accounts/AddressDisplay'
+import { ContextMenu, MenuOptionItem } from 'uniswap/src/components/menus/ContextMenu'
+import { ContextMenuTriggerMode } from 'uniswap/src/components/menus/types'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
import { DisplayNameType } from 'uniswap/src/features/accounts/types'
@@ -18,10 +20,9 @@ import { pushNotification } from 'uniswap/src/features/notifications/slice/slice
import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/slice/types'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
-import { setClipboard } from 'uniswap/src/utils/clipboard'
+import { setClipboard } from 'utilities/src/clipboard/clipboard'
import { NumberType } from 'utilities/src/format/types'
-import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
-import { MenuContentItem } from 'wallet/src/components/menu/types'
+import { useBooleanState } from 'utilities/src/react/useBooleanState'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks'
@@ -41,6 +42,7 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte
const formattedBalance = convertFiatAmountFormatted(balanceUSD, NumberType.PortfolioBalance)
const [showEditLabelModal, setShowEditLabelModal] = useState(false)
+ const { value: isContextMenuOpen, setTrue: openMenu, setFalse: closeMenu } = useBooleanState(false)
const accounts = useSignerAccounts()
const displayName = useDisplayName(address)
@@ -63,30 +65,21 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte
)
}, [accounts, address, dispatch])
- const onPressCopyAddress = useCallback(
- async (e: BaseSyntheticEvent) => {
- // We have to manually prevent click-through because the way the context menu is inside of a TouchableArea in this component it
- // means that without it the TouchableArea handler will get called
- // TODO(EXT-1325): Use a different ContextMenu component that works inside a TouchableArea
- e.preventDefault()
- e.stopPropagation()
-
- await setClipboard(address)
- dispatch(
- pushNotification({
- type: AppNotificationType.Copied,
- copyType: CopyNotificationType.Address,
- }),
- )
- sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
- element: ElementName.CopyAddress,
- modal: ModalName.AccountSwitcher,
- })
- },
- [address, dispatch],
- )
+ const onPressCopyAddress = useCallback(async (): Promise => {
+ await setClipboard(address)
+ dispatch(
+ pushNotification({
+ type: AppNotificationType.Copied,
+ copyType: CopyNotificationType.Address,
+ }),
+ )
+ sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
+ element: ElementName.CopyAddress,
+ modal: ModalName.AccountSwitcher,
+ })
+ }, [address, dispatch])
- const menuOptions = useMemo((): MenuContentItem[] => {
+ const menuOptions = useMemo((): MenuOptionItem[] => {
return [
{
label: t('account.wallet.menu.copy.title'),
@@ -97,12 +90,7 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte
label: !accountHasUnitag
? t('account.wallet.menu.edit.title')
: t('settings.setting.wallet.action.editProfile'),
- onPress: async (e: BaseSyntheticEvent): Promise => {
- // We have to manually prevent click-through because the way the context menu is inside of a TouchableArea in this component it
- // means that without it the TouchableArea handler will get called
- e.preventDefault()
- e.stopPropagation()
-
+ onPress: async (): Promise => {
if (accountHasUnitag) {
await focusOrCreateUnitagTab(address, UnitagClaimRoutes.EditProfile)
} else {
@@ -113,29 +101,20 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte
},
{
label: t('account.wallet.menu.manageConnections'),
- onPress: (e: BaseSyntheticEvent): void => {
- // We have to manually prevent click-through because the way the context menu is inside of a TouchableArea in this component it
- // means that without it the TouchableArea handler will get called
- e.preventDefault()
- e.stopPropagation()
-
- navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`)
+ onPress: async (): Promise => {
+ navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}?address=${address}`)
},
Icon: Globe,
},
{
label: t('account.wallet.menu.remove.title'),
- onPress: (e: BaseSyntheticEvent): void => {
- // We have to manually prevent click-through because the way the context menu is inside of a TouchableArea in this component it
- // means that without it the TouchableArea handler will get called
- e.preventDefault()
- e.stopPropagation()
-
+ onPress: (): void => {
setShowRemoveWalletModal(true)
},
- textProps: { color: '$statusCritical' },
+ textColor: '$statusCritical',
Icon: TrashFilled,
- iconProps: { color: '$statusCritical' },
+ iconColor: '$statusCritical',
+ destructive: true,
},
]
}, [accountHasUnitag, onPressCopyAddress, navigateTo, t, address])
@@ -171,16 +150,22 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte
size={iconSizes.icon40}
variant="subheading2"
/>
-
+
-
+
{formattedBalance}
()
@@ -192,37 +195,35 @@ export function AccountSwitcherScreen(): JSX.Element {
zIndex: 1,
}
- const menuOptions = useMemo((): MenuContentItem[] => {
+ const menuOptions = useMemo((): MenuOptionItem[] => {
return [
...(canClaimUnitag
? [
{
label: t('account.wallet.menu.claimUsername'),
-
- onPress: async () => await focusOrCreateUnitagTab(activeAddress, UnitagClaimRoutes.ClaimIntro),
-
+ onPress: async (): Promise => {
+ await focusOrCreateUnitagTab(activeAddress, UnitagClaimRoutes.ClaimIntro)
+ },
Icon: Person,
},
]
: []),
-
{
label: t('account.wallet.menu.manageConnections'),
- onPress: () => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`),
+ onPress: (): void => {
+ navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}?address=${activeAddress}`)
+ },
Icon: Globe,
},
{
label: t('account.wallet.menu.remove.title'),
- onPress: (e: BaseSyntheticEvent): void => {
- // We have to manually prevent click-through because the way the context menu is inside of a TouchableArea in this component it
- // means that without it the TouchableArea handler will get called
- e.preventDefault()
- e.stopPropagation()
+ onPress: (): void => {
setShowRemoveWalletModal(true)
},
- textProps: { color: '$statusCritical' },
+ textColor: '$statusCritical',
Icon: TrashFilled,
- iconProps: { color: '$statusCritical' },
+ iconColor: '$statusCritical',
+ destructive: true,
},
]
}, [canClaimUnitag, activeAddress, navigateTo, t])
@@ -270,12 +271,11 @@ export function AccountSwitcherScreen(): JSX.Element {
Icon={X}
rightColumn={
-
+ {
+ // oxlint-disable-next-line typescript/await-thenable -- biome-parity: oxlint is stricter here
await dispatch(
editAccountActions.trigger({
type: EditAccountAction.Rename,
diff --git a/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap b/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap
index eed50298533..032019f493e 100644
--- a/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap
+++ b/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap
@@ -46,11 +46,9 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _alignItems-center _pr-t-space-spa94665593 _pl-t-space-spa94665593 _pt-t-space-spa94665589 _pb-t-space-spa94665589 _width-10037"
>